Enable Narrow and Lite regions for EU (#10120)
* Enable Lite and Narrow regions and introduce getEffectiveDutyCycle for Lite profiles * Add TrafficType enum and extend getConfiguredOrDefaultMsScaled to manage based on regionProfile settings * Refactor telemetry modules to include TrafficType in getConfiguredOrDefaultMsScaled calls * Update submodule protobufs to latest commit * Add support for new region presets and modem presets in menu options * Add new LoRa region codes and modem presets for EU bands * boof * Add modem presets for LITE and NARROW configurations * Update subproject commit reference in protobufs * Update protobufs * Refactor modem preset definitions to use macro for consistency and clarity * Refactor modem preset cases to use PRESET macro for consistency * fix: update LoRa region code for EU 868 narrowband configuration Co-authored-by: Copilot <copilot@github.com> * Fix test suite failure Co-authored-by: Copilot <copilot@github.com> * Add override slot override - for when one override isn't enough. Co-authored-by: Copilot <copilot@github.com> * address copilot comments --------- Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
* 6. Channel spacing calculation (placeholder for future protobuf changes)
|
||||
*/
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
@@ -21,6 +22,9 @@
|
||||
|
||||
#include "meshtastic/config.pb.h"
|
||||
|
||||
// hash() is a file-scope function in RadioInterface.cpp; link it in for slot-formula tests
|
||||
extern uint32_t hash(const char *str);
|
||||
|
||||
class MockMeshService : public MeshService
|
||||
{
|
||||
public:
|
||||
@@ -163,20 +167,58 @@ static const RegionProfile TEST_PROFILE_TURBO = {
|
||||
/* overrideSlot */ 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,
|
||||
/* overrideSlot */ OVERRIDE_SLOT_PRESET_HASH,
|
||||
};
|
||||
|
||||
// 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,
|
||||
"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, "TEST_US_SPACED"},
|
||||
{meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, "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"},
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, "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"},
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_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"},
|
||||
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, "TEST_UNSET"},
|
||||
};
|
||||
|
||||
static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||||
@@ -194,6 +236,13 @@ static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode c
|
||||
// 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);
|
||||
@@ -268,6 +317,137 @@ static void test_shadowTable_unknownCodeFallsToSentinel()
|
||||
TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name);
|
||||
}
|
||||
|
||||
static void test_shadowTable_presetHashProfileHasCorrectOverrideSlot()
|
||||
{
|
||||
TEST_ASSERT_EQUAL(OVERRIDE_SLOT_PRESET_HASH, TEST_PROFILE_PRESET_HASH.overrideSlot);
|
||||
TEST_ASSERT_EQUAL(-1, TEST_PROFILE_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
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -769,6 +949,7 @@ void setup()
|
||||
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);
|
||||
@@ -798,6 +979,13 @@ void setup()
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user