diff --git a/.gitignore b/.gitignore index bee3fe4c3..f29da0ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .pio pio +.pio-docker pio.tar web web.tar diff --git a/docs/lora_region_preset_compatibility_client_spec.md b/docs/lora_region_preset_compatibility_client_spec.md index fb2019ec0..0a818ff41 100644 --- a/docs/lora_region_preset_compatibility_client_spec.md +++ b/docs/lora_region_preset_compatibility_client_spec.md @@ -69,7 +69,7 @@ message LoRaRegionPresetMap { ### 2.3 Why grouped (and the size envelope clients should respect) A `FromRadio` packet is capped at **512 bytes** (`MAX_TO_FROM_RADIO_SIZE`). Most regions -share one identical preset list (the "standard" 9-preset list), so the map is delivered +share one identical preset list (the "standard" 10-preset list), so the map is delivered **grouped**: `groups` holds each _distinct_ preset list once, and `region_groups` maps every known region to one of those groups by index. This keeps the encoded size additive (`groups` + `region_groups`) rather than multiplicative, well under the cap. @@ -246,14 +246,14 @@ For decoder unit tests. With the 2.8 region table, the firmware emits **6 groups indices are assigned in region-table order (first region to use a profile creates its group), so they are stable as listed here: -| group_index | default_preset | licensed_only | presets | -| ----------------------- | -------------- | ------------- | -------------------------------------------------------------------------------------------------------------- | -| 0 (standard) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE, SHORT_TURBO, LONG_TURBO | -| 1 (EU 868) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE | -| 2 (EU 866 SRD / "lite") | `LITE_FAST` | false | LITE_FAST, LITE_SLOW | -| 3 (EU 868 narrow) | `NARROW_SLOW` | false | NARROW_FAST, NARROW_SLOW | -| 4 (ham 20 kHz) | `TINY_FAST` | **true** | TINY_FAST, TINY_SLOW | -| 5 (ham 100 kHz) | `NARROW_SLOW` | **true** | NARROW_FAST, NARROW_SLOW | +| group_index | default_preset | licensed_only | presets | +| ----------------------- | -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| 0 (standard) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE, SHORT_TURBO, LONG_TURBO, MEDIUM_TURBO | +| 1 (EU 868) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE | +| 2 (EU 866 SRD / "lite") | `LITE_FAST` | false | LITE_FAST, LITE_SLOW | +| 3 (EU 868 narrow) | `NARROW_SLOW` | false | NARROW_FAST, NARROW_SLOW | +| 4 (ham 20 kHz) | `TINY_FAST` | **true** | TINY_FAST, TINY_SLOW | +| 5 (ham 100 kHz) | `NARROW_SLOW` | **true** | NARROW_FAST, NARROW_SLOW | `region_groups` (region → group_index): diff --git a/protobufs b/protobufs index 1ae3be3d5..9d589c132 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 1ae3be3d5413d2190c1eb0d3ced094237de68b81 +Subproject commit 9d589c1321478193885d6cecf852bf02d14fc92b diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 63ffdc45e..5ab39fc1c 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -27,6 +27,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC case PRESET(MEDIUM_FAST): return useShortName ? "MedF" : "MediumFast"; break; + case PRESET(MEDIUM_TURBO): + return useShortName ? "MedT" : "MediumTurbo"; + break; case PRESET(LONG_SLOW): return useShortName ? "LongS" : "LongSlow"; break; diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index c14b53839..2e2a35f18 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -844,6 +844,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat return -6.0f; case PRESET(MEDIUM_SLOW): case PRESET(MEDIUM_FAST): + case PRESET(MEDIUM_TURBO): return -5.5f; case PRESET(SHORT_SLOW): case PRESET(SHORT_FAST): diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index 7cc8cc58e..e5b54d6a2 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -229,6 +229,11 @@ static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset cr = 5; sf = 10; break; + case PRESET(MEDIUM_TURBO): + bwKHz = wideLora ? 1625.0f : 500.0f; + cr = 5; + sf = 9; + break; case PRESET(LONG_TURBO): bwKHz = wideLora ? 1625.0f : 500.0f; cr = 8; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 5a3215fee..84d0f7906 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -39,8 +39,8 @@ #endif static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = { - PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), - PRESET(SHORT_FAST), PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), MODEM_PRESET_END}; + PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST), + PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), PRESET(MEDIUM_TURBO), MODEM_PRESET_END}; static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = { PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 1f49ef9f3..0778a9bc5 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -374,7 +374,11 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths. Only compatible with SX127x and SX126x chipsets. Comparable link budget and data rate to LONG_MODERATE. */ - meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15 + meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15, + /* Medium Range - Turbo + This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth. + It is not legal to use in all regions due to this wider bandwidth. */ + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO = 16 } meshtastic_Config_LoRaConfig_ModemPreset; typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode { @@ -767,8 +771,8 @@ extern "C" { #define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST -#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW -#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW+1)) +#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO +#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO+1)) #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index b2278133d..7d3116127 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -2118,6 +2118,7 @@ static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) return -6.0f; case PRESET(MEDIUM_SLOW): case PRESET(MEDIUM_FAST): + case PRESET(MEDIUM_TURBO): return -5.5f; case PRESET(SHORT_SLOW): case PRESET(SHORT_FAST): diff --git a/test/test_admin_radio/test_main.cpp b/test/test_admin_radio/test_main.cpp index 8a839f934..06748b7fa 100644 --- a/test/test_admin_radio/test_main.cpp +++ b/test/test_admin_radio/test_main.cpp @@ -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); diff --git a/test/test_fuzz_packets/test_main.cpp b/test/test_fuzz_packets/test_main.cpp index 196962aea..c65b5460e 100644 --- a/test/test_fuzz_packets/test_main.cpp +++ b/test/test_fuzz_packets/test_main.cpp @@ -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; } diff --git a/test/test_mesh_beacon/test_main.cpp b/test/test_mesh_beacon/test_main.cpp index 6cca03fbd..bb1a0abc2 100644 --- a/test/test_mesh_beacon/test_main.cpp +++ b/test/test_mesh_beacon/test_main.cpp @@ -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); diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp index 7d0d9565a..c62e6b267 100644 --- a/test/test_radio/test_main.cpp +++ b/test/test_radio/test_main.cpp @@ -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());