Add MEDIUM_TURBO modem preset (#10988)

* Protobufs

* Wire up MEDIUM_TURBO modem preset

MEDIUM_TURBO (500 kHz, SF9, CR 4/5) already existed in the protobuf enum but
was never wired into firmware, so selecting it silently fell through to the
LONG_FAST default and rendered an "Invalid" display name.

Add its bw/sf/cr mapping (modemPresetToParams), display name (MediumTurbo/MedT),
PRESETS_STD membership (standard regions only — 500 kHz does not fit EU868's
250 kHz band, so it stays out of PRESETS_EU_868 and is rejected/clamped there),
and the MEDIUM SNR-grading bucket. Includes positive coverage in test_radio,
EU868-reject + US-accept coverage in test_admin_radio and test_mesh_beacon,
the STD preset count 9->10, an extended fuzz range, and the client-spec doc.

* Address review feedback on MEDIUM_TURBO tests

- test_mesh_beacon: assert has_mesh_beacon before checking the invalid preset was
  cleared, so the EU868-cleared test can't pass on a dropped message (matches the
  existing SHORT_TURBO test).
- test_fuzz_packets: draw modem presets from _ModemPreset_ARRAYSIZE instead of a
  hard-coded 17 so the fuzz range tracks future enum additions automatically.
This commit is contained in:
Ben Meadors
2026-07-11 08:24:35 -05:00
committed by GitHub
co-authored by GitHub
parent ca833d944c
commit c5355641d3
13 changed files with 129 additions and 25 deletions
+12 -8
View File
@@ -484,7 +484,7 @@ static void test_validateConfigLora_allStdPresetsValidForUS()
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_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
};
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
@@ -498,7 +498,8 @@ static void test_validateConfigLora_allStdPresetsValidForUS()
static void test_validateConfigLora_turboPresetsInvalidForEU868()
{
// EU_868 has PRESETS_EU_868 which excludes SHORT_TURBO and LONG_TURBO
// 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;
@@ -508,6 +509,9 @@ static void test_validateConfigLora_turboPresetsInvalidForEU868()
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()
@@ -598,13 +602,13 @@ static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast()
static void test_validateConfigLora_allPresetsValidForLORA24()
{
// LORA_24 uses PROFILE_STD (9 presets) with wideLora=true
// 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_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
};
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
@@ -792,11 +796,11 @@ static void test_validateConfigLora_siblingLockedPresetStillFailsValidation()
// RegionInfo preset list integrity tests
// -----------------------------------------------------------------------
static void test_presetsStd_hasNineEntries()
static void test_presetsStd_hasTenEntries()
{
// PROFILE_STD should have exactly 9 presets
// 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(9, us->getNumPresets());
TEST_ASSERT_EQUAL(10, us->getNumPresets());
TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets());
}
@@ -1320,7 +1324,7 @@ void setup()
RUN_TEST(test_validateConfigLora_siblingLockedPresetStillFailsValidation);
// RegionInfo preset list integrity
RUN_TEST(test_presetsStd_hasNineEntries);
RUN_TEST(test_presetsStd_hasTenEntries);
RUN_TEST(test_presetsEU868_hasSevenEntries);
RUN_TEST(test_presetsUndef_hasOneEntry);
RUN_TEST(test_defaultPresetIsInAvailablePresets);
+3 -2
View File
@@ -450,7 +450,8 @@ static meshtastic_AdminMessage fuzzAdminMessage()
// manual bandwidth==0 path that used to SIGFPE the validator.
r.set_config.which_payload_variant = meshtastic_Config_lora_tag;
r.set_config.payload_variant.lora.region = (meshtastic_Config_LoRaConfig_RegionCode)rngRange(32);
r.set_config.payload_variant.lora.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)rngRange(16);
r.set_config.payload_variant.lora.modem_preset =
(meshtastic_Config_LoRaConfig_ModemPreset)rngRange(_meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE);
r.set_config.payload_variant.lora.use_preset = (rngRange(2) == 0);
r.set_config.payload_variant.lora.bandwidth = rngRange(512); // includes 0
r.set_config.payload_variant.lora.channel_num = rngNext();
@@ -553,7 +554,7 @@ static meshtastic_MeshBeacon fuzzBeacon()
fuzzChannelSettings(b.offer_channel);
b.offer_region = (meshtastic_Config_LoRaConfig_RegionCode)rngRange(32);
b.has_offer_preset = (rngRange(2) == 0);
b.offer_preset = (meshtastic_Config_LoRaConfig_ModemPreset)rngRange(16);
b.offer_preset = (meshtastic_Config_LoRaConfig_ModemPreset)rngRange(_meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE);
return b;
}
+40
View File
@@ -251,6 +251,44 @@ static void test_adminValidation_turboPresetOnUS_isAccepted(void)
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, moduleConfig.mesh_beacon.broadcast_on_preset);
}
/**
* Verify MEDIUM_TURBO is also cleared for EU_868. Like SHORT_TURBO/LONG_TURBO it is a 500 kHz preset
* that does not fit EU_868's 250 kHz band, so it must not survive admin validation there.
*/
static void test_adminValidation_mediumTurboPresetOnEU868_isCleared(void)
{
resetConfig();
meshtastic_ModuleConfig_MeshBeaconConfig bcfg = meshtastic_ModuleConfig_MeshBeaconConfig_init_zero;
bcfg.has_broadcast_on_preset = true;
bcfg.broadcast_on_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO;
testAdmin->handleSetModuleConfig(makeBeaconModuleConfig(bcfg));
TEST_ASSERT_TRUE(moduleConfig.has_mesh_beacon);
TEST_ASSERT_FALSE(moduleConfig.mesh_beacon.has_broadcast_on_preset);
}
/**
* Verify MEDIUM_TURBO passes validation for US (PROFILE_STD allows the full turbo family).
* The same 500 kHz preset that is illegal in EU_868 must be preserved in permissive regions.
*/
static void test_adminValidation_mediumTurboPresetOnUS_isAccepted(void)
{
resetConfig();
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
initRegion();
meshtastic_ModuleConfig_MeshBeaconConfig bcfg = meshtastic_ModuleConfig_MeshBeaconConfig_init_zero;
bcfg.has_broadcast_on_preset = true;
bcfg.broadcast_on_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO;
testAdmin->handleSetModuleConfig(makeBeaconModuleConfig(bcfg));
TEST_ASSERT_TRUE(moduleConfig.mesh_beacon.has_broadcast_on_preset);
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO, moduleConfig.mesh_beacon.broadcast_on_preset);
}
/**
* Verify an out-of-range region code (255) is sanitised to UNSET rather than stored verbatim.
* Important to prevent invalid proto enum values from reaching the broadcaster and being broadcast
@@ -1360,6 +1398,8 @@ BEACON_TEST_ENTRY void setup()
RUN_TEST(test_adminValidation_turboPresetOnEU868_isCleared);
RUN_TEST(test_adminValidation_longTurboPresetOnEU868_isCleared);
RUN_TEST(test_adminValidation_turboPresetOnUS_isAccepted);
RUN_TEST(test_adminValidation_mediumTurboPresetOnEU868_isCleared);
RUN_TEST(test_adminValidation_mediumTurboPresetOnUS_isAccepted);
RUN_TEST(test_adminValidation_unknownOfferRegion_isCleared);
RUN_TEST(test_adminValidation_validOfferRegion_isPreserved);
RUN_TEST(test_adminValidation_targetUnknownRegion_isCleared);
+44
View File
@@ -195,6 +195,47 @@ static void test_applyModemConfig_customCodingRateLowerThanPreset()
TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr());
}
// MEDIUM_TURBO performs like MEDIUM_FAST (sf=9, cr=5) but at 500 kHz. Verify the params resolve.
static void test_applyModemConfig_mediumTurbo()
{
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_MEDIUM_TURBO;
testRadio->reconfigure();
TEST_ASSERT_EQUAL_UINT8(5, testRadio->getCr());
TEST_ASSERT_EQUAL_UINT8(9, testRadio->getSf());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 500.0f, testRadio->getBw());
}
// MEDIUM_TURBO is a 500 kHz preset, so it is invalid for EU_868 and must clamp to the region default.
static void test_clampConfigLora_mediumTurboInvalidForEU868()
{
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_MEDIUM_TURBO;
RadioInterface::clampConfigLora(cfg);
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
}
// MEDIUM_TURBO is valid for US (PROFILE_STD) and must be left unchanged.
static void test_clampConfigLora_mediumTurboValidForUS()
{
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_TURBO;
RadioInterface::clampConfigLora(cfg);
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO, cfg.modem_preset);
}
// -----------------------------------------------------------------------
// getRegionPresetMap() - region->valid-preset map sent to clients during want_config
// -----------------------------------------------------------------------
@@ -317,6 +358,9 @@ void setup()
RUN_TEST(test_applyModemConfig_codingRateMatchesPreset);
RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset);
RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset);
RUN_TEST(test_applyModemConfig_mediumTurbo);
RUN_TEST(test_clampConfigLora_mediumTurboInvalidForEU868);
RUN_TEST(test_clampConfigLora_mediumTurboValidForUS);
RUN_TEST(test_regionPresetMap_coversAllRegionsWithinBounds);
RUN_TEST(test_regionPresetMap_matchesRegionTable);
exit(UNITY_END());