Regioninfo (#11056)

* advertise a superset of EU regional presets to client devices.

* trunk
This commit is contained in:
Tom
2026-07-19 06:22:39 -05:00
committed by GitHub
parent a4ea55f919
commit b8b5582943
3 changed files with 79 additions and 29 deletions
@@ -153,7 +153,10 @@ These rules are what keep the UX correct across firmware versions. Implement all
5. **EU region auto-swap caveat.** The firmware treats the EU sibling regions 5. **EU region auto-swap caveat.** The firmware treats the EU sibling regions
(`EU_868` / `EU_866` / `EU_N_868`) specially: if the user is in one of them and selects a (`EU_868` / `EU_866` / `EU_N_868`) specially: if the user is in one of them and selects a
preset that belongs to a sibling's list, the firmware **swaps the region** rather than preset that belongs to a sibling's list, the firmware **swaps the region** rather than
rejecting the preset. Consequence for clients: **do not assume the region is immutable rejecting the preset. To make this visible in the picker, the firmware advertises the
**same superset** (the union of the trio's presets) for all three sibling regions, so a
client filtering per §6 will offer every EU 86x preset regardless of which sibling is
currently selected. Consequence for clients: **do not assume the region is immutable
across a preset change** - after an admin config write, re-read the resulting across a preset change** - after an admin config write, re-read the resulting
`LoRaConfig` and reflect the (possibly changed) region back into the UI. `LoRaConfig` and reflect the (possibly changed) region back into the UI.
@@ -249,12 +252,23 @@ so they are stable as listed here:
| group_index | default_preset | licensed_only | presets | | 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 | | 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 | | 1 (EU 868) | `LONG_FAST` | false | _EU 86x superset_ (see below) |
| 2 (EU 866 SRD / "lite") | `LITE_FAST` | false | LITE_FAST, LITE_SLOW | | 2 (EU 866 SRD / "lite") | `LITE_FAST` | false | _EU 86x superset_ (see below) |
| 3 (EU 868 narrow) | `NARROW_SLOW` | false | NARROW_FAST, NARROW_SLOW | | 3 (EU 868 narrow) | `NARROW_SLOW` | false | _EU 86x superset_ (see below) |
| 4 (ham 20 kHz) | `TINY_FAST` | **true** | TINY_FAST, TINY_SLOW | | 4 (ham 20 kHz) | `TINY_FAST` | **true** | TINY_FAST, TINY_SLOW |
| 5 (ham 100 kHz) | `NARROW_SLOW` | **true** | NARROW_FAST, NARROW_SLOW | | 5 (ham 100 kHz) | `NARROW_SLOW` | **true** | NARROW_FAST, NARROW_SLOW |
The **EU 86x superset** advertised by groups 1, 2 and 3 is the union of the trio's own
band presets, because the firmware auto-swaps region within the trio on preset selection
(§5), so any of these is a legal pick from any of the three regions:
```text
LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE, LITE_FAST, LITE_SLOW, NARROW_FAST, NARROW_SLOW
```
The three groups still differ by `default_preset` (`LONG_FAST` / `LITE_FAST` / `NARROW_SLOW`),
which is why they remain distinct groups despite sharing this preset list.
`region_groups` (region → group_index): `region_groups` (region → group_index):
| group | regions | | group | regions |
@@ -266,9 +280,11 @@ so they are stable as listed here:
| 4 | ITU1_2M, ITU2_2M, ITU3_2M | | 4 | ITU1_2M, ITU2_2M, ITU3_2M |
| 5 | ITU2_125CM | | 5 | ITU2_125CM |
> Note groups **3** and **5** carry the same preset list (NARROW\_\*) but are distinct groups > Note that several groups can carry overlapping preset lists but remain distinct: groups 1,
> because they differ in `licensed_only`. Decoders must key on the group, not on the preset > 2 and 3 share the EU 86x superset yet differ in `default_preset`, and group **5** (ham
> list, to preserve the licensing flag. > 100 kHz) shares the `NARROW_*` presets with group 3 but differs in `licensed_only`.
> Decoders must key on the group, not on the preset list, to preserve `default_preset` and
> the licensing flag.
> >
> Regions **absent** from the table (no constraint info; see §5.1): `EU_874`, `EU_917`, > Regions **absent** from the table (no constraint info; see §5.1): `EU_874`, `EU_917`,
> `ITU1_70CM`, `ITU2_70CM`, `ITU3_70CM`. > `ITU1_70CM`, `ITU2_70CM`, `ITU3_70CM`.
+36 -16
View File
@@ -55,6 +55,34 @@ static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END}; static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
// The EU_868/EU_866/EU_N_868 trio share the 868 band but own mutually exclusive preset
// profiles. Selecting a preset locked to a sibling swaps the region to that sibling (see
// regionSwapForPreset), so from any region in the trio every one of these presets is
// selectable. This union is what we advertise to clients as the trio's legal list. It is a
// display-only superset: on-device enforcement still uses each region's own disjoint
// profile->presets, so this must never be assigned to a RegionProfile (that would make
// supportsPreset() accept the preset in place and defeat the swap). Keep in sync with the
// EU_868/EU_866/EU_N_868 profile lists below. Sized to the 11-preset wire cap.
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_SUPERSET[] = {
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST),
PRESET(LONG_MODERATE), PRESET(LITE_FAST), PRESET(LITE_SLOW), PRESET(NARROW_FAST), PRESET(NARROW_SLOW), MODEM_PRESET_END};
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
// locked to a sibling means the user wants that sibling region, not the default preset.
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
};
static bool isSwappableEuRegion(meshtastic_Config_LoRaConfig_RegionCode code)
{
for (auto c : SWAPPABLE_EU_REGIONS)
if (c == code)
return true;
return false;
}
// Region profiles: bundle preset list + regulatory parameters shared across regions // Region profiles: bundle preset list + regulatory parameters shared across regions
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle // presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1}; const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
@@ -687,8 +715,13 @@ void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map)
grp.default_preset = r->getDefaultPreset(); grp.default_preset = r->getDefaultPreset();
grp.licensed_only = r->profile->licensedOnly; grp.licensed_only = r->profile->licensedOnly;
grp.presets_count = 0; grp.presets_count = 0;
for (size_t i = 0; r->profile->presets[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++) // EU 86x siblings advertise the trio's superset - any of those presets is
grp.presets[grp.presets_count++] = r->profile->presets[i]; // reachable from here via an automatic region swap. Every other region
// advertises exactly its own enforced profile list.
const meshtastic_Config_LoRaConfig_ModemPreset *advertised =
isSwappableEuRegion(r->code) ? PRESETS_EU_SUPERSET : r->profile->presets;
for (size_t i = 0; advertised[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
grp.presets[grp.presets_count++] = advertised[i];
} }
// Map this region to its group (capacity checked at the top of the loop). // Map this region to its group (capacity checked at the top of the loop).
@@ -963,14 +996,6 @@ static void sendErrorNotification(const char *msg, meshtastic_LogRecord_Level le
service->sendClientNotification(cn); service->sendClientNotification(cn);
} }
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
// locked to a sibling means the user wants that sibling region, not the default preset.
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
};
/** /**
* If currentRegion is one of the swappable EU regions and preset belongs to a sibling in * If currentRegion is one of the swappable EU regions and preset belongs to a sibling in
* that trio, return the sibling region that owns the preset. Returns nullptr otherwise. * that trio, return the sibling region that owns the preset. Returns nullptr otherwise.
@@ -978,12 +1003,7 @@ static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
const RegionInfo *RadioInterface::regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion, const RegionInfo *RadioInterface::regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion,
meshtastic_Config_LoRaConfig_ModemPreset preset) meshtastic_Config_LoRaConfig_ModemPreset preset)
{ {
bool currentIsSwappable = false; if (!isSwappableEuRegion(currentRegion))
for (auto code : SWAPPABLE_EU_REGIONS) {
if (code == currentRegion)
currentIsSwappable = true;
}
if (!currentIsSwappable)
return nullptr; return nullptr;
for (auto code : SWAPPABLE_EU_REGIONS) { for (auto code : SWAPPABLE_EU_REGIONS) {
+20 -6
View File
@@ -292,16 +292,30 @@ static void test_regionPresetMap_matchesRegionTable()
const meshtastic_LoRaPresetGroup &grp = map.groups[gi]; const meshtastic_LoRaPresetGroup &grp = map.groups[gi];
const RegionInfo *r = getRegion(code); const RegionInfo *r = getRegion(code);
// Group's list is non-empty, within the generated array bound, and is the // Group's list is non-empty and within the generated array bound.
// region's full list.
const size_t maxPresets = sizeof(grp.presets) / sizeof(grp.presets[0]); const size_t maxPresets = sizeof(grp.presets) / sizeof(grp.presets[0]);
TEST_ASSERT_GREATER_THAN_UINT(0, grp.presets_count); TEST_ASSERT_GREATER_THAN_UINT(0, grp.presets_count);
TEST_ASSERT_LESS_OR_EQUAL_UINT((unsigned)maxPresets, grp.presets_count); TEST_ASSERT_LESS_OR_EQUAL_UINT((unsigned)maxPresets, grp.presets_count);
TEST_ASSERT_EQUAL_UINT((unsigned)r->getNumPresets(), (unsigned)grp.presets_count);
// Every advertised preset is legal in this region. // Every advertised preset must be selectable from this region: either legal here,
for (pb_size_t p = 0; p < grp.presets_count; p++) // or legal in a sibling the firmware will auto-swap us to (the EU 86x trio, which
TEST_ASSERT_TRUE(r->supportsPreset(grp.presets[p])); // advertises the union of the trio's presets rather than just its own).
for (pb_size_t p = 0; p < grp.presets_count; p++) {
bool selectable =
r->supportsPreset(grp.presets[p]) || RadioInterface::regionSwapForPreset(code, grp.presets[p]) != nullptr;
TEST_ASSERT_TRUE(selectable);
}
// The region's own enforced presets must all be advertised (advertised is a
// superset of the enforced list, never a subset).
const meshtastic_Config_LoRaConfig_ModemPreset *enforced = r->getAvailablePresets();
for (size_t e = 0; e < r->getNumPresets(); e++) {
bool advertised = false;
for (pb_size_t p = 0; p < grp.presets_count; p++)
if (grp.presets[p] == enforced[e])
advertised = true;
TEST_ASSERT_TRUE(advertised);
}
// Default preset matches the table, is legal, and is present in the list. // Default preset matches the table, is legal, and is present in the list.
TEST_ASSERT_EQUAL(r->getDefaultPreset(), grp.default_preset); TEST_ASSERT_EQUAL(r->getDefaultPreset(), grp.default_preset);