mirror of
https://github.com/meshtastic/firmware.git
synced 2026-03-29 12:33:28 -04:00
* Enhance LoRa configuration with modem presets and validation logic * Rename bootstrapLoRaConfigFromPreset tests to validateModemConfig for clarity and consistency * additional tidy-ups to the validateModemConfig - still fundamentally broken at this point * Enhance region validation by adding numPresets to RegionInfo and implementing validateRegionConfig in RadioInterface * Add validation for modem configuration in applyModemConfig * Fix region unset handling and improve modem config validation in handleSetConfig * Refactor LoRa configuration validation methods and introduce clamping method for invalid settings * Update handleSetConfig to use fromOthers parameter to either correct or reject invalid settings * Fix some of the copilot review comments for LoRa configuration validation and clamping methods; add tests for region and preset handling * Redid the slot default checking and calculation. Should resolve the outstanding comments. * Add bandwidth calculation for LoRa modem preset fallback in clampConfigLora * Remove unused preset name variable in validateConfigLora and fix default frequency slot check in applyModemConfig * update tests for region handling * Got the synthetic colleague to add mock service for testing * Flash savings... hopefully * Refactor modem preset handling to use sentinel values and improve default preset retrieval * Refactor region handling to use profile structures for modem presets and channel calculations * added comments for clarity on parameters * Add shadow table tests and validateConfigLora enhancements for region presets * Add isFromUs tests for preset validation in AdminModule * Respond to copilot github review * address copilot comments * address null poointers * fix build errors * Fix the fix, undo the silly suggestions from synthetic reviewer. * we all float here * Fix include path for AdminModule in test_main.cpp * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * More suggestion fixes * admin module merge conflicts * admin module fixes from merge hell * fix: initialize default frequency slot and custom channel name; update LNA mode handling * save some bytes... * fix: simplify error logging for bandwidth checks in LoRa configuration * Update src/mesh/MeshRadio.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
815 lines
33 KiB
C++
815 lines
33 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 "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "RadioInterface.h"
|
|
#include "TestUtil.h"
|
|
#include "modules/AdminModule.h"
|
|
#include <unity.h>
|
|
|
|
#include "meshtastic/config.pb.h"
|
|
|
|
class MockMeshService : public MeshService
|
|
{
|
|
public:
|
|
void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); }
|
|
};
|
|
|
|
static MockMeshService *mockMeshService;
|
|
|
|
// -----------------------------------------------------------------------
|
|
// getRegion() tests
|
|
// -----------------------------------------------------------------------
|
|
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
|
|
|
|
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));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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,
|
|
/* overrideSlot */ 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,
|
|
/* overrideSlot */ 3,
|
|
};
|
|
|
|
// 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,
|
|
/* overrideSlot */ 0,
|
|
};
|
|
|
|
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, "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,
|
|
"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,
|
|
"TEST_LORA24_TURBO"},
|
|
|
|
// Sentinel — must be last
|
|
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "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
|
|
// -----------------------------------------------------------------------
|
|
|
|
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->profile->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);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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,
|
|
};
|
|
|
|
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 SHORT_TURBO and LONG_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");
|
|
}
|
|
|
|
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 (9 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,
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RegionInfo preset list integrity tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_presetsStd_hasNineEntries()
|
|
{
|
|
// PROFILE_STD should have exactly 9 presets
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(9, 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
|
|
// -----------------------------------------------------------------------
|
|
|
|
class AdminModuleTestShim : public AdminModule
|
|
{
|
|
public:
|
|
using AdminModule::handleSetConfig;
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test runner
|
|
// -----------------------------------------------------------------------
|
|
|
|
void setUp(void)
|
|
{
|
|
mockMeshService = new MockMeshService();
|
|
service = mockMeshService;
|
|
testAdmin = new AdminModuleTestShim();
|
|
}
|
|
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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// RegionInfo preset list integrity
|
|
RUN_TEST(test_presetsStd_hasNineEntries);
|
|
RUN_TEST(test_presetsEU868_hasSevenEntries);
|
|
RUN_TEST(test_presetsUndef_hasOneEntry);
|
|
RUN_TEST(test_defaultPresetIsInAvailablePresets);
|
|
RUN_TEST(test_regionFieldsAreSane);
|
|
RUN_TEST(test_onlyLORA24HasWideLora);
|
|
|
|
// 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);
|
|
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|