#include "MeshRadio.h" #include "MeshService.h" #include "RadioInterface.h" #include "TestUtil.h" #include #include "meshtastic/config.pb.h" class MockMeshService : public MeshService { public: void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } }; static MockMeshService *mockMeshService; // Test shim to expose protected radio parameters set by applyModemConfig() class TestableRadioInterface : public RadioInterface { public: TestableRadioInterface() : RadioInterface() {} uint8_t getCr() const { return cr; } uint8_t getSf() const { return sf; } float getBw() const { return bw; } // Override reconfigure to call the base which invokes applyModemConfig() bool reconfigure() override { return RadioInterface::reconfigure(); } // Stubs for pure virtual methods required by RadioInterface uint32_t getPacketTime(uint32_t, bool) override { return 0; } ErrorCode send(meshtastic_MeshPacket *p) override { return ERRNO_OK; } }; static void test_bwCodeToKHz_specialMappings() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 62.5f, bwCodeToKHz(62)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 203.125f, bwCodeToKHz(200)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 406.25f, bwCodeToKHz(400)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 812.5f, bwCodeToKHz(800)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 1625.0f, bwCodeToKHz(1600)); } static void test_bwCodeToKHz_passthrough() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 125.0f, bwCodeToKHz(125)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250)); } static void test_validateConfigLora_noopWhenUsePresetFalse() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = false; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; cfg.bandwidth = 123; cfg.spread_factor = 8; RadioInterface::validateConfigLora(cfg); TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth); TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } static void test_validateConfigLora_validPreset_nonWideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_validPreset_wideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_rejectsInvalidPresetForRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); } static void test_clampConfigLora_invalidPresetClampedToDefault() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); } static void test_clampConfigLora_validPresetUnchanged() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } // ----------------------------------------------------------------------- // applyModemConfig() coding rate tests (via reconfigure) // ----------------------------------------------------------------------- static TestableRadioInterface *testRadio; // After fresh flash: coding_rate=0, use_preset=true, modem_preset=LONG_FAST // CR should come from the preset (5 for LONG_FAST), not from the zero default. static void test_applyModemConfig_freshFlashCodingRateNotZero() { 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; // coding_rate is 0 (default after init_zero, same as fresh flash) testRadio->reconfigure(); // LONG_FAST preset has cr=5; must never be 0 TEST_ASSERT_EQUAL_UINT8(5, testRadio->getCr()); TEST_ASSERT_EQUAL_UINT8(11, testRadio->getSf()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 250.0f, testRadio->getBw()); } // When coding_rate matches the preset exactly, should still use the preset value static void test_applyModemConfig_codingRateMatchesPreset() { 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_SLOW; config.lora.coding_rate = 8; // LONG_SLOW default is cr=8 testRadio->reconfigure(); TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr()); } // Custom CR higher than preset should be used static void test_applyModemConfig_customCodingRateHigherThanPreset() { 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.coding_rate = 7; // LONG_FAST preset has cr=5, 7 > 5 testRadio->reconfigure(); TEST_ASSERT_EQUAL_UINT8(7, testRadio->getCr()); } // Custom CR lower than preset: preset wins (higher is more robust) static void test_applyModemConfig_customCodingRateLowerThanPreset() { 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_SLOW; config.lora.coding_rate = 5; // LONG_SLOW preset has cr=8, 5 < 8 testRadio->reconfigure(); TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr()); } void setUp(void) { mockMeshService = new MockMeshService(); service = mockMeshService; // RadioInterface computes slotTimeMsec during construction and expects myRegion to be valid. config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; initRegion(); testRadio = new TestableRadioInterface(); } void tearDown(void) { delete testRadio; testRadio = nullptr; service = nullptr; delete mockMeshService; mockMeshService = nullptr; } void setup() { delay(10); delay(2000); initializeTestEnvironment(); UNITY_BEGIN(); RUN_TEST(test_bwCodeToKHz_specialMappings); RUN_TEST(test_bwCodeToKHz_passthrough); RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse); RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion); RUN_TEST(test_validateConfigLora_validPreset_wideRegion); RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion); RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); RUN_TEST(test_clampConfigLora_validPresetUnchanged); RUN_TEST(test_applyModemConfig_freshFlashCodingRateNotZero); RUN_TEST(test_applyModemConfig_codingRateMatchesPreset); RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset); RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset); exit(UNITY_END()); } void loop() {}