mirror of
https://github.com/meshtastic/firmware.git
synced 2026-08-01 10:58:30 -04:00
* added warnings from firmware for simple channel setting mistakes * more and better checks * one ping only * Drop literal 'AQ==' from blank-PSK channel warnings The base64 encoding of the default channel key isn't something a user should type in by hand; state the condition instead of a raw key value. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1561 lines
67 KiB
C++
1561 lines
67 KiB
C++
/**
|
|
* Tests for the radio configuration validation and clamping functions
|
|
* introduced in the radio_interface_cherrypick branch.
|
|
*
|
|
* Targets:
|
|
* 1. getRegion()
|
|
* 2. RadioInterface::validateConfigRegion()
|
|
* 3. RadioInterface::validateConfigLora()
|
|
* 4. RadioInterface::clampConfigLora()
|
|
* 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF)
|
|
* 6. Channel spacing calculation (placeholder for future protobuf changes)
|
|
*/
|
|
|
|
#include "DisplayFormatters.h"
|
|
#include "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "RadioInterface.h"
|
|
#include "TestUtil.h"
|
|
#include "mesh/Channels.h"
|
|
#include "modules/AdminModule.h"
|
|
#include <string>
|
|
#include <unity.h>
|
|
#include <vector>
|
|
|
|
#include "meshtastic/config.pb.h"
|
|
#include "support/AdminModuleTestShim.h"
|
|
|
|
// hash() is a file-scope function in RadioInterface.cpp; link it in for slot-formula tests
|
|
extern uint32_t hash(const char *str);
|
|
|
|
// Every client notification the AdminModule emits flows through sendClientNotification();
|
|
// capture each formatted message so the warning/coalescing tests can assert on the exact
|
|
// set of messages produced by a sequence of admin messages. This shadows test/support/MockMeshService.h's
|
|
// release-only stub because these tests need to inspect the captured message text, not just avoid leaks.
|
|
static std::vector<std::string> capturedWarnings;
|
|
|
|
class MockMeshService : public MeshService
|
|
{
|
|
public:
|
|
void sendClientNotification(meshtastic_ClientNotification *n) override
|
|
{
|
|
capturedWarnings.push_back(n->message);
|
|
releaseClientNotificationToPool(n);
|
|
}
|
|
};
|
|
|
|
static MockMeshService *mockMeshService;
|
|
|
|
// -----------------------------------------------------------------------
|
|
// getRegion() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_getRegion_returnsCorrectRegion_US()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("US", r->name);
|
|
}
|
|
|
|
static void test_getRegion_returnsCorrectRegion_EU868()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("EU_868", r->name);
|
|
}
|
|
|
|
static void test_getRegion_returnsCorrectRegion_LORA24()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code);
|
|
TEST_ASSERT_TRUE(r->wideLora);
|
|
}
|
|
|
|
static void test_getRegion_unsetCodeReturnsUnsetEntry()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("UNSET", r->name);
|
|
}
|
|
|
|
static void test_getRegion_unknownCodeFallsToUnset()
|
|
{
|
|
// A code not in the table should iterate to the UNSET sentinel
|
|
const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// validateConfigRegion() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_validateConfigRegion_validRegionReturnsTrue()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
|
|
// Ensure owner is not licensed (should not matter for non-licensed-only regions)
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
static void test_validateConfigRegion_unsetRegionReturnsTrue()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
// UNSET region has licensedOnly=false, so should pass
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
static void test_validateConfigRegion_unknownCodeReturnsFalse()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)255;
|
|
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
// Unknown code is not in the regions table; getRegion() returns the UNSET sentinel,
|
|
// whose .code != 255, so validateConfigRegion should reject it.
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
static void test_validateConfigRegion_anotherUnknownCodeReturnsFalse()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)99;
|
|
|
|
devicestate.owner.is_licensed = true;
|
|
|
|
// Unknown code should be rejected even when owner is licensed.
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Shadow tables for testing (preset lists → profiles → regions → lookup)
|
|
// -----------------------------------------------------------------------
|
|
|
|
// A minimal preset list with only one entry
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// A preset list that includes all turbo variants only
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// A restricted list simulating a hypothetical tight-regulation region
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// Mirrors PROFILE_STD but with non-zero spacing/padding for testing
|
|
static const RegionProfile TEST_PROFILE_SPACED = {
|
|
TEST_PRESETS_SINGLE,
|
|
/* spacing */ 0.025f,
|
|
/* padding */ 0.010f,
|
|
/* audioPermitted */ true,
|
|
/* licensedOnly */ false,
|
|
/* textThrottle */ 0,
|
|
/* positionThrottle */ 0,
|
|
/* telemetryThrottle */ 0,
|
|
};
|
|
|
|
// A licensed-only profile for testing access control
|
|
static const RegionProfile TEST_PROFILE_LICENSED = {
|
|
TEST_PRESETS_RESTRICTED,
|
|
/* spacing */ 0.0f,
|
|
/* padding */ 0.0f,
|
|
/* audioPermitted */ false,
|
|
/* licensedOnly */ true,
|
|
/* textThrottle */ 5,
|
|
/* positionThrottle */ 10,
|
|
/* telemetryThrottle */ 10,
|
|
};
|
|
|
|
// Turbo-only profile
|
|
static const RegionProfile TEST_PROFILE_TURBO = {
|
|
TEST_PRESETS_TURBO_ONLY,
|
|
/* spacing */ 0.0f,
|
|
/* padding */ 0.0f,
|
|
/* audioPermitted */ true,
|
|
/* licensedOnly */ false,
|
|
/* textThrottle */ 0,
|
|
/* positionThrottle */ 0,
|
|
/* telemetryThrottle */ 0,
|
|
};
|
|
|
|
// A preset list for the preset-hash override slot test (LONG_FAST + MEDIUM_FAST)
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_PRESET_HASH[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// Profile with overrideSlot = OVERRIDE_SLOT_PRESET_HASH (-1):
|
|
// slot selection always uses hash(presetDisplayName), ignoring the primary channel name.
|
|
static const RegionProfile TEST_PROFILE_PRESET_HASH = {
|
|
TEST_PRESETS_PRESET_HASH,
|
|
/* spacing */ 0.0f,
|
|
/* padding */ 0.0f,
|
|
/* audioPermitted */ true,
|
|
/* licensedOnly */ false,
|
|
/* textThrottle */ 0,
|
|
/* positionThrottle */ 0,
|
|
/* telemetryThrottle */ 0,
|
|
};
|
|
|
|
// Standalone test region using US frequencies (26 MHz span → 104 slots at 250 kHz BW)
|
|
// Used to verify OVERRIDE_SLOT_PRESET_HASH slot formula; not inserted into testRegions[].
|
|
static const RegionInfo TEST_REGION_PRESET_HASH = {
|
|
meshtastic_Config_LoRaConfig_RegionCode_US,
|
|
902.0f,
|
|
928.0f,
|
|
100,
|
|
30,
|
|
false,
|
|
false,
|
|
&TEST_PROFILE_PRESET_HASH,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
OVERRIDE_SLOT_PRESET_HASH,
|
|
"TEST_PRESET_HASH",
|
|
};
|
|
|
|
static const RegionInfo testRegions[] = {
|
|
// A wide US-like region with spacing + padding
|
|
{meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_US_SPACED"},
|
|
|
|
// A narrow band simulating tight EU regulation
|
|
{meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, 3, "TEST_EU_LICENSED"},
|
|
|
|
// A wide-LoRa region with turbo-only presets
|
|
{meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, 0, "TEST_LORA24_TURBO"},
|
|
|
|
// Sentinel - must be last
|
|
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_UNSET"},
|
|
};
|
|
|
|
static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
|
{
|
|
const RegionInfo *r = testRegions;
|
|
while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
if (r->code == code)
|
|
return r;
|
|
r++;
|
|
}
|
|
return r; // Returns the UNSET sentinel
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Shadow table tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
// Helper: replicate the numFreqSlots formula from RadioInterface so tests can compute expected values.
|
|
static uint32_t testComputeNumFreqSlots(const RegionInfo *r, float bw_kHz)
|
|
{
|
|
float w = r->profile->spacing + (r->profile->padding * 2) + (bw_kHz / 1000.0f);
|
|
return (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / w) + 0.5f);
|
|
}
|
|
|
|
static void test_shadowTable_spacedProfileHasNonZeroSpacing()
|
|
{
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding);
|
|
}
|
|
|
|
static void test_shadowTable_licensedProfileFlagsCorrect()
|
|
{
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_TRUE(r->profile->licensedOnly);
|
|
TEST_ASSERT_FALSE(r->profile->audioPermitted);
|
|
TEST_ASSERT_EQUAL(3, r->overrideSlot);
|
|
}
|
|
|
|
static void test_shadowTable_presetCountMatchesExpected()
|
|
{
|
|
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(1, spaced->getNumPresets());
|
|
|
|
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(2, licensed->getNumPresets());
|
|
|
|
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_EQUAL(2, turbo->getNumPresets());
|
|
}
|
|
|
|
static void test_shadowTable_defaultPresetIsFirstInList()
|
|
{
|
|
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset());
|
|
|
|
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset());
|
|
|
|
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset());
|
|
}
|
|
|
|
static void test_shadowTable_channelSpacingWithPadding()
|
|
{
|
|
// Verify channel count when spacing + padding are non-zero
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
|
float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f);
|
|
|
|
// spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250
|
|
// channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing);
|
|
|
|
uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f);
|
|
// (928 - 902 + 0.025) / 0.295 = 88.2 → 88
|
|
TEST_ASSERT_EQUAL_UINT32(88, numChannels);
|
|
}
|
|
|
|
static void test_shadowTable_turboOnlyOnWideLora()
|
|
{
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_TRUE(r->wideLora);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset());
|
|
|
|
// Verify wide-LoRa bandwidth for SHORT_TURBO
|
|
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode
|
|
}
|
|
|
|
static void test_shadowTable_unknownCodeFallsToSentinel()
|
|
{
|
|
const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name);
|
|
}
|
|
|
|
static void test_shadowTable_presetHashProfileHasCorrectOverrideSlot()
|
|
{
|
|
TEST_ASSERT_EQUAL(OVERRIDE_SLOT_PRESET_HASH, TEST_REGION_PRESET_HASH.overrideSlot);
|
|
TEST_ASSERT_EQUAL(-1, TEST_REGION_PRESET_HASH.overrideSlot);
|
|
TEST_ASSERT_EQUAL(2, TEST_REGION_PRESET_HASH.getNumPresets());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// OVERRIDE_SLOT_PRESET_HASH (-1) slot formula tests
|
|
//
|
|
// Property under test:
|
|
// overrideSlot = -1 → slot = hash(presetDisplayName) % numSlots
|
|
// regardless of what the primary channel is named
|
|
// overrideSlot = 0 → slot = hash(channelName) % numSlots
|
|
// when channel name = preset display name, these two modes give identical slots
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_overrideSlotPresetHash_longFast_customChannelMatchesDefaultNameSlot()
|
|
{
|
|
// US + LONG_FAST: spacing=0, padding=0, bw=250 kHz
|
|
// numSlots = round((928-902+0)/0.250) = 104
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
TEST_ASSERT_EQUAL_UINT32(104, numSlots); // sanity
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
|
|
|
|
// OVERRIDE_SLOT_PRESET_HASH (-1):
|
|
// channel is "MyCustomNetwork" but slot still uses preset name hash
|
|
uint32_t slotPresetHashMode = hash(presetName) % numSlots;
|
|
|
|
// OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH (0) with channel name = preset name (user never renamed it):
|
|
// channelName == presetName → same hash → same slot
|
|
const char *defaultChannelName = presetName;
|
|
uint32_t slotChannelHashModeDefaultName = hash(defaultChannelName) % numSlots;
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(slotPresetHashMode, slotChannelHashModeDefaultName);
|
|
|
|
// Confirm a different custom channel name gives a different hash INPUT
|
|
// (so mode 0 would diverge while mode -1 stays locked)
|
|
TEST_ASSERT_TRUE(strcmp(presetName, "MyCustomNetwork") != 0);
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_mediumFast_customChannelMatchesDefaultNameSlot()
|
|
{
|
|
// US + MEDIUM_FAST: bw=250 kHz → same 104 slots as LONG_FAST for US
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
TEST_ASSERT_EQUAL_UINT32(104, numSlots); // sanity
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
|
|
|
|
// Mode -1: slot = hash(presetName) % numSlots (channel name irrelevant)
|
|
uint32_t slotPresetHashMode = hash(presetName) % numSlots;
|
|
|
|
// Mode 0 + default name (channel name = preset display name):
|
|
uint32_t slotChannelHashModeDefaultName = hash(presetName) % numSlots;
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(slotPresetHashMode, slotChannelHashModeDefaultName);
|
|
|
|
TEST_ASSERT_TRUE(strcmp(presetName, "MyCustomNetwork") != 0);
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_longFast_slotIsStableAcrossCustomNames()
|
|
{
|
|
// Mode -1 must give the same slot for LONG_FAST regardless of which custom name is in use.
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
|
|
uint32_t expectedSlot = hash(presetName) % numSlots;
|
|
|
|
// Simulate three different custom channel names; mode -1 ignores all of them
|
|
const char *customNames[] = {"AlphaNet", "BetaMesh", "GammaMesh"};
|
|
for (int i = 0; i < 3; i++) {
|
|
uint32_t slotForCustom = hash(presetName) % numSlots; // mode -1: presetName only
|
|
TEST_ASSERT_EQUAL_UINT32(expectedSlot, slotForCustom);
|
|
// Confirm input would have differed in mode 0
|
|
TEST_ASSERT_TRUE(strcmp(presetName, customNames[i]) != 0);
|
|
}
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_mediumFast_slotIsStableAcrossCustomNames()
|
|
{
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
|
|
uint32_t expectedSlot = hash(presetName) % numSlots;
|
|
|
|
const char *customNames[] = {"AlphaNet", "BetaMesh", "GammaMesh"};
|
|
for (int i = 0; i < 3; i++) {
|
|
uint32_t slotForCustom = hash(presetName) % numSlots; // mode -1: presetName only
|
|
TEST_ASSERT_EQUAL_UINT32(expectedSlot, slotForCustom);
|
|
TEST_ASSERT_TRUE(strcmp(presetName, customNames[i]) != 0);
|
|
}
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_longFastAndMediumFast_slotsAreDifferentPresets()
|
|
{
|
|
// LONG_FAST and MEDIUM_FAST have different display names → likely different hash slots.
|
|
// This verifies the two presets genuinely occupy distinct positions, so the equivalence
|
|
// tests above are not trivially vacuous.
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw_lf = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false);
|
|
float bw_mf = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false);
|
|
uint32_t numSlots_lf = testComputeNumFreqSlots(us, bw_lf);
|
|
uint32_t numSlots_mf = testComputeNumFreqSlots(us, bw_mf);
|
|
|
|
const char *nameLF =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
|
|
const char *nameMF =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
|
|
|
|
TEST_ASSERT_TRUE(strcmp(nameLF, nameMF) != 0);
|
|
|
|
uint32_t slotLF = hash(nameLF) % numSlots_lf;
|
|
uint32_t slotMF = hash(nameMF) % numSlots_mf;
|
|
// They use the same numSlots (both 250 kHz on US), so a difference in display name
|
|
// should produce a different slot.
|
|
TEST_ASSERT_NOT_EQUAL(slotLF, slotMF);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// validateConfigLora() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_validateConfigLora_validPresetForUS()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_allStdPresetsValidForUS()
|
|
{
|
|
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = stdPresets[i];
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US");
|
|
}
|
|
}
|
|
|
|
static void test_validateConfigLora_turboPresetsInvalidForEU868()
|
|
{
|
|
// EU_868 has PRESETS_EU_868 which excludes the 500 kHz turbo presets
|
|
// (SHORT_TURBO, LONG_TURBO, MEDIUM_TURBO)
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_TURBO should be invalid for EU_868");
|
|
}
|
|
|
|
static void test_validateConfigLora_validPresetsForEU868()
|
|
{
|
|
meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) {
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = eu868Presets[i];
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868");
|
|
}
|
|
}
|
|
|
|
static void test_validateConfigLora_customBandwidthTooWideForEU868()
|
|
{
|
|
// EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz
|
|
// A 500 kHz custom BW should be rejected
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 500;
|
|
cfg.spread_factor = 11;
|
|
cfg.coding_rate = 5;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_customBandwidthFitsUS()
|
|
{
|
|
// US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 250;
|
|
cfg.spread_factor = 11;
|
|
cfg.coding_rate = 5;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_customBandwidthFitsEU868()
|
|
{
|
|
// EU_868 spans 250 kHz, 125 kHz BW should fit
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 125;
|
|
cfg.spread_factor = 12;
|
|
cfg.coding_rate = 8;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_bogusPresetRejected()
|
|
{
|
|
// A fabricated preset value not in any list should be rejected
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast()
|
|
{
|
|
// UNSET uses PROFILE_UNDEF which has only LONG_FAST
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
cfg.use_preset = true;
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET");
|
|
}
|
|
|
|
static void test_validateConfigLora_allPresetsValidForLORA24()
|
|
{
|
|
// LORA_24 uses PROFILE_STD (10 presets) with wideLora=true
|
|
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = stdPresets[i];
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24");
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// clampConfigLora() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_clampConfigLora_invalidPresetClampedToDefault()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_validPresetUnchanged()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_customBwTooWideClampedToDefaultBw()
|
|
{
|
|
// EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW.
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 500;
|
|
cfg.spread_factor = 11;
|
|
cfg.coding_rate = 5;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth);
|
|
}
|
|
|
|
static void test_clampConfigLora_customBwValidLeftUnchanged()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 125;
|
|
cfg.spread_factor = 12;
|
|
cfg.coding_rate = 8;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
|
|
}
|
|
|
|
static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast()
|
|
{
|
|
// UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault()
|
|
{
|
|
// LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD)
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Region-locked preset swap tests (EU_868 / EU_866 / EU_N_868 trio)
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_clampConfigLora_narrowPresetOnEU866SwapsToEUN868()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_litePresetOnEU868SwapsToEU866()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_866, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_eu868PresetOnEUN868SwapsToEU868()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_N_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_litePresetOnUSDoesNotSwap()
|
|
{
|
|
// Previous region is not one of the swappable trio, so the preset clamps to the
|
|
// region default instead of swapping regions.
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, cfg.region);
|
|
TEST_ASSERT_EQUAL(us->getDefaultPreset(), cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_narrowPresetOnHam125cmDoesNotSwap()
|
|
{
|
|
// ITU2_125CM shares the NARROW presets, so they are valid there and nothing changes
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_validateConfigLora_siblingLockedPresetStillFailsValidation()
|
|
{
|
|
// Validation (no clamp) must keep failing so callers route into clampConfigLora,
|
|
// which performs the region swap.
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RegionInfo preset list integrity tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_presetsStd_hasTenEntries()
|
|
{
|
|
// PROFILE_STD should have exactly 10 presets (adds MEDIUM_TURBO to the turbo cluster)
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(10, us->getNumPresets());
|
|
TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets());
|
|
}
|
|
|
|
static void test_presetsEU868_hasSevenEntries()
|
|
{
|
|
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(7, eu->getNumPresets());
|
|
TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets());
|
|
}
|
|
|
|
static void test_presetsUndef_hasOneEntry()
|
|
{
|
|
const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
TEST_ASSERT_EQUAL(1, unset->getNumPresets());
|
|
TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets());
|
|
}
|
|
|
|
static void test_defaultPresetIsInAvailablePresets()
|
|
{
|
|
// For every region, the defaultPreset must appear in its own availablePresets list
|
|
const RegionInfo *r = regions;
|
|
while (true) {
|
|
bool found = false;
|
|
for (size_t i = 0; i < r->getNumPresets(); i++) {
|
|
if (r->getAvailablePresets()[i] == r->getDefaultPreset()) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
char msg[80];
|
|
snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(found, msg);
|
|
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
break; // UNSET is the sentinel, stop after it
|
|
r++;
|
|
}
|
|
}
|
|
|
|
static void test_regionFieldsAreSane()
|
|
{
|
|
// Basic sanity check: all regions have freqEnd > freqStart and a non-null name
|
|
const RegionInfo *r = regions;
|
|
while (true) {
|
|
char msg[80];
|
|
snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg);
|
|
TEST_ASSERT_NOT_NULL(r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0");
|
|
TEST_ASSERT_NOT_NULL(r->getAvailablePresets());
|
|
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
break;
|
|
r++;
|
|
}
|
|
}
|
|
|
|
static void test_onlyLORA24HasWideLora()
|
|
{
|
|
// Verify that LORA_24 is the only region with wideLora=true
|
|
const RegionInfo *r = regions;
|
|
while (true) {
|
|
char msg[80];
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
|
|
snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg);
|
|
} else {
|
|
snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name);
|
|
TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg);
|
|
}
|
|
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
break;
|
|
r++;
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Channel spacing calculation (placeholder for future protobuf updates)
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_channelSpacingCalculation_US_LONG_FAST()
|
|
{
|
|
// Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000)
|
|
// US: spacing=0, padding=0
|
|
// LONG_FAST on non-wide region: bw=250 kHz
|
|
// channelSpacing = 0 + 0 + 0.250 = 0.250 MHz
|
|
// numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f);
|
|
uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f);
|
|
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
|
TEST_ASSERT_EQUAL_UINT32(104, numChannels);
|
|
}
|
|
|
|
static void test_channelSpacingCalculation_EU868_LONG_FAST()
|
|
{
|
|
// EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0
|
|
// LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz
|
|
// numChannels = round((0.25 + 0) / 0.250) = 1
|
|
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora);
|
|
float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f);
|
|
uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f);
|
|
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
|
TEST_ASSERT_EQUAL_UINT32(1, numChannels);
|
|
}
|
|
|
|
// Placeholder: when protobuf region definitions include non-zero padding/spacing,
|
|
// add tests here to verify the channel count and frequency calculations.
|
|
static void test_channelSpacingCalculation_placeholder()
|
|
{
|
|
// TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values,
|
|
// verify:
|
|
// - Channel count matches expected value for each (region, preset) pair
|
|
// - First channel frequency = freqStart + (bw/2000) + padding
|
|
// - Nth channel frequency = first + (n * channelSpacing)
|
|
// - overrideSlot, when non-zero, forces the channel_num
|
|
TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// handleSetConfig fromOthers dispatch tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
// AdminModuleTestShim comes from test/support - the friend seam AdminModule.h declares.
|
|
static AdminModuleTestShim *testAdmin;
|
|
|
|
static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset,
|
|
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
|
{
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_lora_tag;
|
|
c.payload_variant.lora.region = region;
|
|
c.payload_variant.lora.use_preset = usePreset;
|
|
c.payload_variant.lora.modem_preset = preset;
|
|
return c;
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_invalidPresetRejected()
|
|
{
|
|
// Set up a known-good baseline in the global config
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
// Build an admin set_config with an invalid preset for EU_868
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
// fromOthers=true: invalid preset should be rejected, old preset preserved
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromLocal_invalidPresetClamped()
|
|
{
|
|
// Set up a known-good baseline
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
// Build an admin set_config with an invalid preset for EU_868
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
|
|
|
testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)
|
|
|
|
// fromOthers=false: invalid preset should be clamped to the region's default
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_validPresetAccepted()
|
|
{
|
|
// Set up baseline
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
// Build an admin set_config with a valid preset for EU_868
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
// Valid preset should be accepted regardless of fromOthers
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_invalidChannelNumFullyRejected()
|
|
{
|
|
// Rejecting a remote config must reject ALL of it: an invalid channel_num must not
|
|
// leak into config.lora alongside the restored region/preset.
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
config.lora.channel_num = 0;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.channel_num = 5000; // far beyond US slot count
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, config.lora.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
TEST_ASSERT_EQUAL_UINT32(0, config.lora.channel_num);
|
|
}
|
|
|
|
// clampBandwidthCode: an unset (0) bandwidth code maps to the default; any other code is left as-is.
|
|
static void test_clampBandwidthCode_zeroMapsToDefaultOthersUnchanged()
|
|
{
|
|
TEST_ASSERT_NOT_EQUAL_UINT16(0, clampBandwidthCode(0)); // the point of the fix: 0 must not stay 0
|
|
TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), clampBandwidthCode(0));
|
|
TEST_ASSERT_EQUAL_UINT16(250, clampBandwidthCode(250));
|
|
TEST_ASSERT_EQUAL_UINT16(125, clampBandwidthCode(125));
|
|
TEST_ASSERT_EQUAL_UINT16(31, clampBandwidthCode(31));
|
|
}
|
|
|
|
// A custom (non-preset) config that leaves bandwidth at its proto zero-value must not persist as 0.
|
|
// Pre-fix it slipped past validateConfigLora() and the radio silently ran at the default while
|
|
// get_config still reported bandwidth 0. It is now coerced to the default code on ingest.
|
|
static void test_handleSetConfig_fromLocal_customBandwidthZeroClampedToDefault()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.spread_factor = 11;
|
|
c.payload_variant.lora.coding_rate = 5;
|
|
c.payload_variant.lora.bandwidth = 0; // the footgun: unset custom bandwidth
|
|
|
|
testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)
|
|
|
|
TEST_ASSERT_FALSE(config.lora.use_preset);
|
|
TEST_ASSERT_NOT_EQUAL_UINT16(0, config.lora.bandwidth); // must not persist as 0
|
|
TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), config.lora.bandwidth);
|
|
}
|
|
|
|
// Remote admin (fromOthers) is subject to the same ingest clamp: a custom bandwidth 0 from another
|
|
// node is normalized to the default rather than persisted as 0 (it does not weaken the wholesale
|
|
// rejection of configs that actually fail validation - a 0 bandwidth already passed validation).
|
|
static void test_handleSetConfig_fromOthers_customBandwidthZeroClampedToDefault()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.spread_factor = 11;
|
|
c.payload_variant.lora.coding_rate = 5;
|
|
c.payload_variant.lora.bandwidth = 0;
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_FALSE(config.lora.use_preset);
|
|
TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), config.lora.bandwidth);
|
|
}
|
|
|
|
// In preset mode bandwidth 0 is the norm (the preset supplies it); the ingest clamp must leave it
|
|
// untouched so preset configs still read back bandwidth 0.
|
|
static void test_handleSetConfig_fromLocal_presetBandwidthZeroLeftUntouched()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.bandwidth = 0;
|
|
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
TEST_ASSERT_TRUE(config.lora.use_preset);
|
|
TEST_ASSERT_EQUAL_UINT16(0, config.lora.bandwidth);
|
|
}
|
|
|
|
// A custom (non-preset) config with an already-valid bandwidth must be preserved verbatim.
|
|
static void test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.spread_factor = 11;
|
|
c.payload_variant.lora.coding_rate = 5;
|
|
c.payload_variant.lora.bandwidth = 125;
|
|
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
TEST_ASSERT_FALSE(config.lora.use_preset);
|
|
TEST_ASSERT_EQUAL_UINT16(125, config.lora.bandwidth);
|
|
}
|
|
|
|
// A security-config SET that omits the private key (partial/legacy client editing some other security field)
|
|
// must NOT regenerate our keypair: our NodeNum is crc32(public_key), so a new keypair would silently change
|
|
// our identity. The existing keypair has to be preserved.
|
|
static void test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted()
|
|
{
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.security.private_key.size = 32;
|
|
memset(config.security.private_key.bytes, 0x11, 32);
|
|
config.security.public_key.size = 32;
|
|
memset(config.security.public_key.bytes, 0x22, 32);
|
|
|
|
// Incoming SET carries no private/public key, just another security field.
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_security_tag;
|
|
c.payload_variant.security.serial_enabled = true;
|
|
|
|
testAdmin->deferSaves();
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
uint8_t expectedPriv[32];
|
|
memset(expectedPriv, 0x11, 32);
|
|
uint8_t expectedPub[32];
|
|
memset(expectedPub, 0x22, 32);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.private_key.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.public_key.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32);
|
|
// The non-key field still applies.
|
|
TEST_ASSERT_TRUE(config.security.serial_enabled);
|
|
}
|
|
|
|
// A SET that DOES supply a full 32-byte keypair (legitimate key import) must apply it, not preserve the old one.
|
|
static void test_handleSetConfig_security_acceptsSuppliedKeypair()
|
|
{
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.security.private_key.size = 32;
|
|
memset(config.security.private_key.bytes, 0x11, 32);
|
|
config.security.public_key.size = 32;
|
|
memset(config.security.public_key.bytes, 0x22, 32);
|
|
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_security_tag;
|
|
c.payload_variant.security.private_key.size = 32;
|
|
memset(c.payload_variant.security.private_key.bytes, 0x33, 32);
|
|
c.payload_variant.security.public_key.size = 32;
|
|
memset(c.payload_variant.security.public_key.bytes, 0x44, 32);
|
|
|
|
testAdmin->deferSaves();
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
uint8_t expectedPriv[32];
|
|
memset(expectedPriv, 0x33, 32);
|
|
uint8_t expectedPub[32];
|
|
memset(expectedPub, 0x44, 32);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32);
|
|
}
|
|
|
|
static void test_regionInfo_supportsPreset()
|
|
{
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_TRUE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
|
|
TEST_ASSERT_FALSE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO));
|
|
TEST_ASSERT_FALSE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST));
|
|
|
|
const RegionInfo *eu866 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
|
|
TEST_ASSERT_TRUE(eu866->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW));
|
|
TEST_ASSERT_FALSE(eu866->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
|
|
}
|
|
|
|
static void test_checkConfigRegion_quietCheckReportsReason()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
TEST_ASSERT_TRUE(RadioInterface::checkConfigRegion(cfg));
|
|
|
|
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)254;
|
|
char err[160] = {0};
|
|
TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg, err, sizeof(err)));
|
|
TEST_ASSERT_TRUE_MESSAGE(strlen(err) > 0, "Expected a failure reason in errBuf");
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion()
|
|
{
|
|
// Baseline: EU_866 (LITE profile)
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST;
|
|
initRegion();
|
|
|
|
// Remote admin keeps the region but selects a NARROW preset (locked to EU_N_868)
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_866, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868, config.lora.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST, config.lora.modem_preset);
|
|
|
|
// Restore the region table pointer for subsequent tests
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
initRegion();
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_lockedPresetFromNonTrioRegionRejected()
|
|
{
|
|
// Baseline: US is not one of the swappable trio, so a LITE preset must be rejected
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, config.lora.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Channel-configuration warning + coalescing tests
|
|
//
|
|
// These exercise the real incoming-admin-message path (handleReceivedProtobuf):
|
|
// begin_edit_settings / set_channel / commit_edit_settings. Warnings raised while a
|
|
// transaction is open must be deferred and collapsed into a single notification at
|
|
// commit; outside a transaction each save emits its own single message immediately.
|
|
// -----------------------------------------------------------------------
|
|
|
|
static const uint8_t DEFAULT_KEY[] = {0x01}; // the well-known "default" PSK (AQ==)
|
|
static const uint8_t CUSTOM_KEY[] = {0x42, 0x17}; // any non-default key
|
|
|
|
// Count captured warnings whose text contains substr.
|
|
static int warningsContaining(const char *substr)
|
|
{
|
|
int n = 0;
|
|
for (const auto &w : capturedWarnings)
|
|
if (w.find(substr) != std::string::npos)
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
static meshtastic_Channel makeChannel(int8_t index, meshtastic_Channel_Role role, const char *name, const uint8_t *psk,
|
|
size_t pskLen)
|
|
{
|
|
meshtastic_Channel ch = meshtastic_Channel_init_zero;
|
|
ch.index = index;
|
|
ch.role = role;
|
|
ch.has_settings = true;
|
|
strncpy(ch.settings.name, name, sizeof(ch.settings.name) - 1);
|
|
ch.settings.psk.size = pskLen;
|
|
for (size_t i = 0; i < pskLen; i++)
|
|
ch.settings.psk.bytes[i] = psk[i];
|
|
return ch;
|
|
}
|
|
|
|
// Dispatch one admin message as if it arrived from a local (from==0) client, which bypasses
|
|
// the passkey/authorization gates so the switch body runs.
|
|
static void sendAdmin(meshtastic_AdminMessage &m)
|
|
{
|
|
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
|
|
mp.from = 0;
|
|
mp.which_payload_variant = meshtastic_MeshPacket_decoded_tag; // required: handler drops non-decoded packets
|
|
testAdmin->handleReceivedProtobuf(mp, &m);
|
|
}
|
|
|
|
static void sendSetChannel(const meshtastic_Channel &ch)
|
|
{
|
|
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
|
m.which_payload_variant = meshtastic_AdminMessage_set_channel_tag;
|
|
m.set_channel = ch;
|
|
sendAdmin(m);
|
|
}
|
|
|
|
static void sendBeginEdit()
|
|
{
|
|
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
|
m.which_payload_variant = meshtastic_AdminMessage_begin_edit_settings_tag;
|
|
m.begin_edit_settings = true;
|
|
sendAdmin(m);
|
|
}
|
|
|
|
static void sendCommitEdit()
|
|
{
|
|
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
|
m.which_payload_variant = meshtastic_AdminMessage_commit_edit_settings_tag;
|
|
m.commit_edit_settings = true;
|
|
sendAdmin(m);
|
|
}
|
|
|
|
// Preset = LongFast on US, unlicensed owner. "LongFast" is the display name we compare against.
|
|
static void usePresetLongFast()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
owner.is_licensed = false;
|
|
}
|
|
|
|
static void test_warn_singleChannel_variantName_oneSpecificMessage()
|
|
{
|
|
usePresetLongFast();
|
|
// Name is a case/space variant of the preset with the default key: a single name issue.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
|
|
}
|
|
|
|
static void test_warn_singleChannel_nameAndPsk_collapsedToCatchAll()
|
|
{
|
|
usePresetLongFast();
|
|
// Variant name AND a non-default key: two issues on one channel collapse to one catch-all.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", CUSTOM_KEY, 2));
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name and PSK issues on channel 0"));
|
|
}
|
|
|
|
static void test_warn_cleanChannel_noMessage()
|
|
{
|
|
usePresetLongFast();
|
|
// Exact preset name + default key: nothing to warn about.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "LongFast", DEFAULT_KEY, 1));
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
}
|
|
|
|
static void test_warn_transaction_multipleChannels_singleCoalescedMessage()
|
|
{
|
|
usePresetLongFast();
|
|
sendBeginEdit();
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
|
|
sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "long fast", DEFAULT_KEY, 1));
|
|
// Nothing emitted yet - warnings are deferred until commit.
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
|
|
sendCommitEdit();
|
|
// Exactly one message, naming both channels.
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name issues on channels 0, 1"));
|
|
}
|
|
|
|
static void test_warn_transaction_singleChannel_keepsSpecificMessage()
|
|
{
|
|
usePresetLongFast();
|
|
sendBeginEdit();
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
|
|
sendCommitEdit();
|
|
// One flagged channel: the specific message verbatim, not the plural catch-all.
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
|
|
TEST_ASSERT_EQUAL_INT(0, warningsContaining("on channels"));
|
|
}
|
|
|
|
static void test_warn_license_noTransaction_emittedImmediately()
|
|
{
|
|
usePresetLongFast();
|
|
owner.is_licensed = true;
|
|
// Setting a channel that still carries a key triggers ensureLicensedOperation() to strip it.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2));
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("Licensed mode activated"));
|
|
}
|
|
|
|
static void test_warn_license_transaction_coalescedToSingleMessage()
|
|
{
|
|
usePresetLongFast();
|
|
owner.is_licensed = true;
|
|
sendBeginEdit();
|
|
// Two separate triggers within one transaction (two channels with keys to strip).
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2));
|
|
sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "", CUSTOM_KEY, 2));
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
|
|
sendCommitEdit();
|
|
// Collapsed to a single licensed-mode notice (and no channel warning, since names are blank).
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("Licensed mode activated"));
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test runner
|
|
// -----------------------------------------------------------------------
|
|
|
|
void setUp(void)
|
|
{
|
|
mockMeshService = new MockMeshService();
|
|
service = mockMeshService;
|
|
testAdmin = new AdminModuleTestShim();
|
|
capturedWarnings.clear();
|
|
// Committing an edit transaction triggers a full saveToDisk(), which dereferences nodeDB.
|
|
// Create it once (kept reachable via the global, so no leak) for the warning tests; the
|
|
// other tests in this suite set their own config/region state and are unaffected.
|
|
if (!nodeDB)
|
|
nodeDB = new NodeDB();
|
|
}
|
|
void tearDown(void)
|
|
{
|
|
service = nullptr;
|
|
delete mockMeshService;
|
|
mockMeshService = nullptr;
|
|
delete testAdmin;
|
|
testAdmin = nullptr;
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
delay(10);
|
|
delay(2000);
|
|
|
|
initializeTestEnvironment();
|
|
|
|
UNITY_BEGIN();
|
|
|
|
// getRegion()
|
|
RUN_TEST(test_getRegion_returnsCorrectRegion_US);
|
|
RUN_TEST(test_getRegion_returnsCorrectRegion_EU868);
|
|
RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24);
|
|
RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry);
|
|
RUN_TEST(test_getRegion_unknownCodeFallsToUnset);
|
|
|
|
// validateConfigRegion()
|
|
RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
|
|
RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
|
|
RUN_TEST(test_validateConfigRegion_unknownCodeReturnsFalse);
|
|
RUN_TEST(test_validateConfigRegion_anotherUnknownCodeReturnsFalse);
|
|
|
|
// Shadow table tests
|
|
RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
|
|
RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect);
|
|
RUN_TEST(test_shadowTable_presetCountMatchesExpected);
|
|
RUN_TEST(test_shadowTable_defaultPresetIsFirstInList);
|
|
RUN_TEST(test_shadowTable_channelSpacingWithPadding);
|
|
RUN_TEST(test_shadowTable_turboOnlyOnWideLora);
|
|
RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel);
|
|
RUN_TEST(test_shadowTable_presetHashProfileHasCorrectOverrideSlot);
|
|
|
|
// validateConfigLora()
|
|
RUN_TEST(test_validateConfigLora_validPresetForUS);
|
|
RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS);
|
|
RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868);
|
|
RUN_TEST(test_validateConfigLora_validPresetsForEU868);
|
|
RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868);
|
|
RUN_TEST(test_validateConfigLora_customBandwidthFitsUS);
|
|
RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868);
|
|
RUN_TEST(test_validateConfigLora_bogusPresetRejected);
|
|
RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast);
|
|
RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24);
|
|
|
|
// clampConfigLora()
|
|
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
|
|
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
|
|
RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw);
|
|
RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged);
|
|
RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast);
|
|
RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault);
|
|
|
|
// Region-locked preset swap
|
|
RUN_TEST(test_clampConfigLora_narrowPresetOnEU866SwapsToEUN868);
|
|
RUN_TEST(test_clampConfigLora_litePresetOnEU868SwapsToEU866);
|
|
RUN_TEST(test_clampConfigLora_eu868PresetOnEUN868SwapsToEU868);
|
|
RUN_TEST(test_clampConfigLora_litePresetOnUSDoesNotSwap);
|
|
RUN_TEST(test_clampConfigLora_narrowPresetOnHam125cmDoesNotSwap);
|
|
RUN_TEST(test_validateConfigLora_siblingLockedPresetStillFailsValidation);
|
|
|
|
// RegionInfo preset list integrity
|
|
RUN_TEST(test_presetsStd_hasTenEntries);
|
|
RUN_TEST(test_presetsEU868_hasSevenEntries);
|
|
RUN_TEST(test_presetsUndef_hasOneEntry);
|
|
RUN_TEST(test_defaultPresetIsInAvailablePresets);
|
|
RUN_TEST(test_regionFieldsAreSane);
|
|
RUN_TEST(test_onlyLORA24HasWideLora);
|
|
|
|
// OVERRIDE_SLOT_PRESET_HASH (-1) slot formula tests
|
|
RUN_TEST(test_overrideSlotPresetHash_longFast_customChannelMatchesDefaultNameSlot);
|
|
RUN_TEST(test_overrideSlotPresetHash_mediumFast_customChannelMatchesDefaultNameSlot);
|
|
RUN_TEST(test_overrideSlotPresetHash_longFast_slotIsStableAcrossCustomNames);
|
|
RUN_TEST(test_overrideSlotPresetHash_mediumFast_slotIsStableAcrossCustomNames);
|
|
RUN_TEST(test_overrideSlotPresetHash_longFastAndMediumFast_slotsAreDifferentPresets);
|
|
|
|
// Channel spacing (current + placeholder)
|
|
RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST);
|
|
RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST);
|
|
RUN_TEST(test_channelSpacingCalculation_placeholder);
|
|
|
|
// handleSetConfig fromOthers dispatch
|
|
RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_invalidChannelNumFullyRejected);
|
|
RUN_TEST(test_clampBandwidthCode_zeroMapsToDefaultOthersUnchanged);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthZeroClampedToDefault);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_customBandwidthZeroClampedToDefault);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_presetBandwidthZeroLeftUntouched);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved);
|
|
RUN_TEST(test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted);
|
|
RUN_TEST(test_handleSetConfig_security_acceptsSuppliedKeypair);
|
|
RUN_TEST(test_regionInfo_supportsPreset);
|
|
RUN_TEST(test_checkConfigRegion_quietCheckReportsReason);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_lockedPresetFromNonTrioRegionRejected);
|
|
|
|
// Channel-configuration warning + coalescing
|
|
RUN_TEST(test_warn_singleChannel_variantName_oneSpecificMessage);
|
|
RUN_TEST(test_warn_singleChannel_nameAndPsk_collapsedToCatchAll);
|
|
RUN_TEST(test_warn_cleanChannel_noMessage);
|
|
RUN_TEST(test_warn_transaction_multipleChannels_singleCoalescedMessage);
|
|
RUN_TEST(test_warn_transaction_singleChannel_keepsSpecificMessage);
|
|
RUN_TEST(test_warn_license_noTransaction_emittedImmediately);
|
|
RUN_TEST(test_warn_license_transaction_coalescedToSingleMessage);
|
|
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|