Lora region preset map (#10736)

* Added lora region and preset maps

* Protos

* Address PR review feedback

- Log (and break/skip) when the region preset map exceeds its array bounds
  instead of silently dropping regions
- Derive test bounds from the generated nanopb array sizes via sizeof()
  instead of hard-coded magic numbers
- Fix want_config sequence comment (missing comma, STATE_SEND_MODULECONFIG)
- Specify a language on the spec's fenced code blocks (markdownlint MD040)

* Fix want_config stall: handle STATE_SEND_REGION_PRESETS in PhoneAPI::available()

available() had a separate per-state switch that wasn't updated for the new
state, so it returned false ('unexpected state 5') and getFromRadio() was
never called - the config handshake stalled after metadata and the client
timed out. Verified via the native simulator integration test.
This commit is contained in:
Ben Meadors
2026-06-19 19:56:24 -05:00
committed by GitHub
co-authored by GitHub
parent 22072c5f4b
commit dd1ec9d462
9 changed files with 556 additions and 4 deletions
@@ -0,0 +1,277 @@
# LoRa Region → Preset Compatibility — Client Implementation Spec
**Status:** Draft for 2.8 · **Audience:** Meshtastic client app developers (Android first,
Apple second, then web/python) · **Firmware side:** implemented in `firmware`
(`FromRadio.region_presets`, see below).
> This document lives in the firmware repo while the feature is developed. It is meant to
> graduate to `meshtastic/protobufs` (and/or the docs site) alongside the upstream protobuf
> PR that reserves `FromRadio` field **19**.
---
## 1. Why this exists
For 2.8 the LoRa regions and modem presets were reworked. **Not every modem preset is legal
in every region** — narrow EU SRD bands, the EU 868 "narrow" band, amateur/ham bands, and
the 2.4 GHz band each accept only a specific subset of presets. The firmware already
enforces this internally (it clamps or rejects illegal combinations), but until now a client
had no way to _know_ the rules, so a user could pick an illegal region+preset pair in the UI
and only discover the problem after the device silently corrected it.
This feature has the firmware **declare the legal region→preset combinations** to the client
during the `want_config` handshake, so the client UI can constrain the preset picker to the
valid set for the currently selected region (and warn about licensed-only bands). It is
purely advisory metadata — the firmware remains the source of truth and still
validates/clamps on its own.
---
## 2. Protocol additions
Three new messages in `meshtastic/mesh.proto`, plus one new `FromRadio` oneof variant.
### 2.1 `FromRadio.region_presets` (field 19)
```proto
message FromRadio {
uint32 id = 1;
oneof payload_variant {
// ... fields 2..18 unchanged ...
LoRaRegionPresetMap region_presets = 19;
}
}
```
### 2.2 Messages
```proto
// A distinct set of legal modem presets shared by one or more LoRa regions.
message LoRaPresetGroup {
repeated Config.LoRaConfig.ModemPreset presets = 1; // legal presets for this group
Config.LoRaConfig.ModemPreset default_preset = 2; // always one of `presets`
bool licensed_only = 3; // ham/amateur band → warn/gate
}
// Associates a single LoRa region with its preset group (by index).
message LoRaRegionPresets {
Config.LoRaConfig.RegionCode region = 1;
uint32 group_index = 2; // index into LoRaRegionPresetMap.groups
}
// The full map, delivered grouped to fit one FromRadio packet.
message LoRaRegionPresetMap {
repeated LoRaPresetGroup groups = 1; // each distinct preset list
repeated LoRaRegionPresets region_groups = 2; // every known region → a group index
}
```
### 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
**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.
nanopb (firmware) array bounds — clients do **not** need to enforce these, but they bound
what you can receive:
| field | max_count |
| ----------------------------------- | ------------------------------------ |
| `LoRaRegionPresetMap.groups` | 8 |
| `LoRaRegionPresetMap.region_groups` | 38 (= number of `RegionCode` values) |
| `LoRaPresetGroup.presets` | 11 |
---
## 3. When it is delivered
`region_presets` is sent **once** during the `want_config` handshake, as a single
`FromRadio` message, in this position:
```text
my_info → (deviceuiConfig) → node_info(self) → metadata → region_presets → channel… → config… → moduleConfig… → node_info(others)… → fileInfo… → config_complete_id → (live packets)
```
i.e. **immediately after `metadata` and before the first `channel`**.
- It is included for a normal full `want_config` and for the **config-only** nonce.
- It is **omitted** for the **nodes-only** nonce (that path skips metadata/config entirely).
- A client must **not** assume it always arrives (see §5).
---
## 4. Decoding into a usable lookup
Flatten the grouped wire form into `Map<RegionCode, RegionPresetInfo>`:
```text
struct RegionPresetInfo { Set<ModemPreset> presets; ModemPreset default; bool licensedOnly }
fun decode(map: LoRaRegionPresetMap): Map<RegionCode, RegionPresetInfo> {
result = {}
for (rg in map.region_groups) {
if (rg.group_index >= map.groups.size) continue // defensive: malformed/forward data
g = map.groups[rg.group_index]
result[rg.region] = RegionPresetInfo(
presets = g.presets.toSet(),
default = g.default_preset,
licensedOnly = g.licensed_only)
}
return result
}
```
Persist this map alongside the rest of the downloaded config so the LoRa config screen can
read it synchronously.
---
## 5. Semantics & rules (the load-bearing part)
These rules are what keep the UX correct across firmware versions. Implement all of them.
1. **Absent region ⇒ no constraint.** If a `RegionCode` does not appear in `region_groups`,
the client has _no_ compatibility info for it and **must not restrict** its preset
choices (fall back to allowing the full `ModemPreset` list). This happens for a handful
of `RegionCode` enum values that have no firmware band table entry (today: `EU_874`,
`EU_917`, `ITU1_70CM`, `ITU2_70CM`, `ITU3_70CM`).
2. **Absent message ⇒ no constraint.** Firmware older than 2.8 never sends `region_presets`.
New clients **must** tolerate the message being absent entirely and keep their existing
(unconstrained) behavior. Do not block the config screen waiting for it.
3. **`default_preset`** is always a member of that group's `presets`. Use it to pre-select a
preset when the user switches to a region whose valid set does not include the currently
selected preset (instead of leaving an illegal selection or guessing).
4. **`licensed_only`** marks ham/amateur bands. Surface a warning or gate (the firmware also
requires the operator's `is_licensed` flag for these regions; coordinate the two so the
user isn't allowed to pick a licensed band without acknowledging licensing).
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
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
across a preset change** — after an admin config write, re-read the resulting
`LoRaConfig` and reflect the (possibly changed) region back into the UI.
6. **Use it as a UI guard, not a validator of truth.** The firmware still validates/clamps
on its own. The map exists to prevent the user from _selecting_ an illegal combo; it is
not a security or correctness boundary.
---
## 6. UI/UX recommendations
- In the LoRa config screen, when a region is selected, **filter/enable the modem-preset
picker to that region's `presets`** (when `use_preset`/`use_modem_preset` is on).
- If the current preset is not in the newly selected region's set, switch the selection to
that region's `default_preset`.
- Show a **licensed badge / confirmation** for regions where `licensed_only == true`.
- If a region is absent from the map (rule §5.1) or the whole message is absent (§5.2),
render the full preset list as before — never show an empty picker.
---
## 7. Forward / backward compatibility
- **Old clients, new firmware:** an unknown `FromRadio` oneof variant (field 19) is ignored
by protobuf/nanopb decoders; the relative ordering of the known messages is unchanged, so
existing apps are unaffected.
- **New clients, old firmware:** message simply never arrives → treat as "no constraints"
(§5.2).
- **Enum growth:** new `RegionCode`/`ModemPreset` values may appear over time. Decoders
should pass through unknown enum values rather than crashing; an unknown region in
`region_groups` is harmless (the client just won't have a localized name for it).
---
## 8. Platform notes
> Verified against the `main` branch of each repo. Both have been refactored away from
> older layouts; re-pin file paths against a specific commit if you need them durable.
### 8.1 Android — `meshtastic/Meshtastic-Android` (Kotlin / Compose, KMP)
- **Protobufs are a published Maven artifact, _not_ a submodule.** Declared in
`gradle/libs.versions.toml` (`org.meshtastic:protobufs`, currently `2.7.25`); generated
package is **`org.meshtastic.proto`**. **A `region_presets`-aware build requires a new
published `org.meshtastic:protobufs` release**, then bumping that one version string.
- **The protobufs are Wire-generated**, so the `FromRadio` oneof is **not** a
`payloadVariantCase` enum — each arm is a **nullable field**. Handle the new variant in
`FromRadioPacketHandlerImpl.handleFromRadio(...)`
(`core/data/.../manager/FromRadioPacketHandlerImpl.kt`) by adding a
`regionPresets != null -> …` arm to the existing `when { … }`, delegating to a handler
(mirror `handleLocalMetadata` / `handleConfigComplete`).
- **State holder:** expose the decoded map from `RadioConfigRepository` /
`RadioConfigRepositoryImpl` as a `Flow` (mirroring `localConfigFlow`/`channelSetFlow`),
consumed by `feature/settings/.../radio/RadioConfigViewModel.kt`.
- **UI:** the region & preset dropdowns are `DropDownPreference`s in
`feature/settings/.../radio/component/LoRaConfigItemList.kt` (public composable
`LoRaConfigScreen`). Gate/filter the `ChannelOption` (preset) dropdown by the selected
`RegionInfo`'s entry in the map.
### 8.2 Apple — `meshtastic/Meshtastic-Apple` (Swift / SwiftUI)
- **Protobufs are vendored** into a local Swift package `MeshtasticProtobufs`
(`MeshtasticProtobufs/Sources/meshtastic/*.pb.swift`), generated from the `protobufs` git
submodule via `scripts/gen_protos.sh`. **To get field 19:** advance the `protobufs`
submodule, run `scripts/gen_protos.sh`, commit the regenerated `.pb.swift` + submodule
pointer. (No published-artifact dependency — Apple can regenerate from any commit.)
- **Dispatch:** `AccessoryManager.processFromRadio(_:)`
(`Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift`) is a real
`switch decodedInfo.payloadVariant { … }` — add a `.regionPresets` case, with the handler
in `AccessoryManager+FromRadio.swift` (mirror `handleConfig` / `handleMetadata`).
- **Persistence:** config is **SwiftData** (`@Model` entities), upserted via
`MeshPackets`/`UpdateSwiftData.swift`. Store the decoded map (e.g. on a settings/connection
model) so the LoRa view can read it.
- **UI:** `Meshtastic/Views/Settings/Config/LoRaConfig.swift` (`struct LoRaConfig: View`)
has the `Picker("Region", …)` (`RegionCodes.userSelectable`) and `Picker("Presets", …)`
(`ModemPresets.userSelectable`, gated on `usePreset`). Filter the presets picker by the
selected region's entry. Enums live in `Meshtastic/Enums/LoraConfigEnums.swift`.
### 8.3 Other clients
- **python (`meshtastic` / Meshtastic-python)** and **web** consume the published protobufs;
they will see `region_presets` once their protobuf dependency includes field 19, and can
ignore it until then (it decodes as an unknown field).
---
## 9. Reference payload (current firmware table)
For decoder unit tests. With the 2.8 region table, the firmware emits **6 groups**. Group
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 |
`region_groups` (region → group_index):
| group | regions |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | US, EU_433, CN, JP, ANZ, ANZ_433, RU, KR, TW, IN, NZ_865, TH, UA_433, UA_868, MY_433, MY_919, SG_923, PH_433, PH_868, PH_915, KZ_433, KZ_863, NP_865, BR_902, LORA_24 |
| 1 | EU_868 |
| 2 | EU_866 |
| 3 | EU_N_868 |
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
| 5 | ITU2_125CM |
> Note groups **3** and **5** carry the same preset list (NARROW\_\*) but are distinct groups
> because they differ in `licensed_only`. Decoders must key on the group, not on the preset
> list, to preserve the licensing flag.
>
> Regions **absent** from the table (no constraint info; see §5.1): `EU_874`, `EU_917`,
> `ITU1_70CM`, `ITU2_70CM`, `ITU3_70CM`.
This table is generated from the firmware's region table at runtime; treat the firmware as
authoritative and these values as the expected snapshot for the 2.8 table.
+5
View File
@@ -88,6 +88,11 @@ extern const RegionInfo *myRegion;
extern void initRegion();
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
// Fill `map` with the region->valid-preset table, grouped so regions sharing a
// preset list reference the same group. Sent to clients during want_config so
// their UI can block illegal region+preset combinations.
extern void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map);
// Valid LoRa spread factor range and defaults
constexpr uint8_t LORA_SF_MIN = 5;
constexpr uint8_t LORA_SF_MAX = 12;
+18 -2
View File
@@ -12,6 +12,7 @@
#include "Channels.h"
#include "Default.h"
#include "FSCommon.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PacketHistory.h"
@@ -516,9 +517,10 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
STATE_SEND_UIDATA,
STATE_SEND_OWN_NODEINFO,
STATE_SEND_METADATA,
STATE_SEND_CHANNELS
STATE_SEND_REGION_PRESETS, // region -> valid modem presets (one message)
STATE_SEND_CHANNELS,
STATE_SEND_CONFIG,
STATE_SEND_MODULE_CONFIG,
STATE_SEND_MODULECONFIG,
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to the client
STATE_SEND_FILEMANIFEST,
STATE_SEND_COMPLETE_ID,
@@ -636,7 +638,20 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
memset(&fromRadioScratch.metadata, 0, sizeof(fromRadioScratch.metadata));
}
#endif
state = STATE_SEND_REGION_PRESETS;
break;
case STATE_SEND_REGION_PRESETS:
// Tell the client which modem presets are legal in each region so its UI
// can block illegal region+preset combinations. This is public RF /
// regulatory information (region and modem_preset are already in the
// unauthenticated LoRa whitelist below), so it is sent unconditionally —
// even an unauthorized/locked-down client can render a correct picker.
LOG_DEBUG("Send region preset map");
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_region_presets_tag;
getRegionPresetMap(fromRadioScratch.region_presets);
state = STATE_SEND_CHANNELS;
config_state = 0; // STATE_SEND_CHANNELS indexes channels starting at 0
break;
case STATE_SEND_CHANNELS:
@@ -1517,6 +1532,7 @@ bool PhoneAPI::available()
case STATE_SEND_CONFIG:
case STATE_SEND_MODULECONFIG:
case STATE_SEND_METADATA:
case STATE_SEND_REGION_PRESETS:
case STATE_SEND_OWN_NODEINFO:
case STATE_SEND_FILEMANIFEST:
case STATE_SEND_COMPLETE_ID:
+1
View File
@@ -46,6 +46,7 @@ class PhoneAPI
STATE_SEND_MY_INFO, // send our my info record
STATE_SEND_OWN_NODEINFO,
STATE_SEND_METADATA,
STATE_SEND_REGION_PRESETS, // Send the region->valid-preset map (one message)
STATE_SEND_CHANNELS, // Send all channels
STATE_SEND_CONFIG, // Replacement for the old Radioconfig
STATE_SEND_MODULECONFIG, // Send Module specific config
+56
View File
@@ -605,6 +605,62 @@ const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
return r;
}
void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map)
{
map = meshtastic_LoRaRegionPresetMap_init_zero;
const size_t maxGroups = sizeof(map.groups) / sizeof(map.groups[0]);
const size_t maxRegions = sizeof(map.region_groups) / sizeof(map.region_groups[0]);
const size_t maxPresets = sizeof(map.groups[0].presets) / sizeof(map.groups[0].presets[0]);
// Coalesce regions that share an identical preset list into one group. Two
// regions belong to the same group when they share the same RegionProfile
// (which owns the preset list + licensing) AND the same default preset.
// Keyed by profile pointer, not the preset-array pointer: PROFILE_NARROW and
// PROFILE_HAM_100KHZ share PRESETS_NARROW but differ in licensedOnly.
const RegionProfile *groupProfile[sizeof(map.groups) / sizeof(map.groups[0])] = {};
for (const RegionInfo *r = regions; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET; r++) {
// No room left to map any further region; once full we can't add more, so
// log once and stop. An incomplete map means clients won't constrain the
// omitted regions, so this must be discoverable rather than silent.
if (map.region_groups_count >= maxRegions) {
LOG_ERROR("Region preset map full at %u regions; remaining regions omitted", (unsigned)maxRegions);
break;
}
// Find the group this region belongs to, or create it.
int gi = -1;
for (pb_size_t g = 0; g < map.groups_count; g++) {
if (groupProfile[g] == r->profile && map.groups[g].default_preset == r->getDefaultPreset()) {
gi = g;
break;
}
}
if (gi < 0) {
if (map.groups_count >= maxGroups) {
// Out of group slots (should not happen for the current table). The
// region can't be advertised; skip it but make the gap visible.
LOG_ERROR("Region preset map out of group slots (%u); region %d omitted", (unsigned)maxGroups, r->code);
continue;
}
gi = map.groups_count++;
groupProfile[gi] = r->profile;
meshtastic_LoRaPresetGroup &grp = map.groups[gi];
grp.default_preset = r->getDefaultPreset();
grp.licensed_only = r->profile->licensedOnly;
grp.presets_count = 0;
for (size_t i = 0; r->profile->presets[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
grp.presets[grp.presets_count++] = r->profile->presets[i];
}
// Map this region to its group (capacity checked at the top of the loop).
meshtastic_LoRaRegionPresets &rg = map.region_groups[map.region_groups_count++];
rg.region = r->code;
rg.group_index = (uint8_t)gi;
}
}
/**
* Get duty cycle for current region. EU_866: 10% for routers, 2.5% for mobile.
*/
@@ -96,6 +96,15 @@ PB_BIND(meshtastic_Neighbor, meshtastic_Neighbor, AUTO)
PB_BIND(meshtastic_DeviceMetadata, meshtastic_DeviceMetadata, AUTO)
PB_BIND(meshtastic_LoRaPresetGroup, meshtastic_LoRaPresetGroup, AUTO)
PB_BIND(meshtastic_LoRaRegionPresets, meshtastic_LoRaRegionPresets, AUTO)
PB_BIND(meshtastic_LoRaRegionPresetMap, meshtastic_LoRaRegionPresetMap, 2)
PB_BIND(meshtastic_Heartbeat, meshtastic_Heartbeat, AUTO)
+106 -1
View File
@@ -1355,6 +1355,53 @@ typedef struct _meshtastic_DeviceMetadata {
uint32_t excluded_modules;
} meshtastic_DeviceMetadata;
/* A distinct set of legal modem presets shared by one or more LoRa regions.
Regions that have an identical preset list / default / licensing reference
the same group (by index) via LoRaRegionPresetMap.region_groups. This keeps
the whole map small enough to fit in a single FromRadio packet, since most
regions share the one standard preset list. */
typedef struct _meshtastic_LoRaPresetGroup {
/* The modem presets that are legal for every region referencing this group. */
pb_size_t presets_count;
meshtastic_Config_LoRaConfig_ModemPreset presets[11];
/* The firmware's default modem preset for regions in this group.
Always one of `presets`. Clients should select this when switching to one
of these regions, or when the current preset is not legal in the new region. */
meshtastic_Config_LoRaConfig_ModemPreset default_preset;
/* True if regions referencing this group are for licensed operators only
(e.g. amateur / ham radio bands). Clients should warn or gate accordingly. */
bool licensed_only;
} meshtastic_LoRaPresetGroup;
/* Associates a single LoRa region with its preset group. */
typedef struct _meshtastic_LoRaRegionPresets {
/* The LoRa region this entry describes. */
meshtastic_Config_LoRaConfig_RegionCode region;
/* Index into LoRaRegionPresetMap.groups for the preset list that is legal
in `region`. */
uint8_t group_index;
} meshtastic_LoRaRegionPresets;
/* Map describing which modem presets are valid for each LoRa region. Sent by
the firmware during the want_config handshake (as FromRadio.region_presets)
so that client UIs can prevent illegal region+preset selections.
Delivery is grouped to save space: `groups` holds each distinct preset list,
and `region_groups` maps every known region to one of those groups by index.
A region that does NOT appear in `region_groups` carries no constraint
information and should not be restricted by the client (e.g. firmware that
predates this message, or a region with no firmware table entry). Clients
must also tolerate this whole message being absent. */
typedef struct _meshtastic_LoRaRegionPresetMap {
/* One entry per distinct (preset-list, default, licensing) combination.
Referenced by index from `region_groups`. */
pb_size_t groups_count;
meshtastic_LoRaPresetGroup groups[8];
/* One entry per known LoRa region, pointing at its preset group. */
pb_size_t region_groups_count;
meshtastic_LoRaRegionPresets region_groups[38];
} meshtastic_LoRaRegionPresetMap;
/* Packets from the radio to the phone will appear on the fromRadio characteristic.
It will support READ and NOTIFY. When a new packet arrives the device will BLE notify?
It will sit in that descriptor until consumed by the phone,
@@ -1411,6 +1458,12 @@ typedef struct _meshtastic_FromRadio {
to report success or failure. Replaces the earlier scheme of
encoding state as magic-string prefixes inside ClientNotification. */
meshtastic_LockdownStatus lockdown_status;
/* Map of which modem presets are legal in each LoRa region. Sent once
during the want_config handshake (right after `metadata`, before the
first `channel`) so client UIs can prevent the user from selecting an
illegal region+preset combination. A region that does not appear in
any group carries no constraint info and should not be restricted. */
meshtastic_LoRaRegionPresetMap region_presets;
};
} meshtastic_FromRadio;
@@ -1604,6 +1657,12 @@ extern "C" {
#define meshtastic_DeviceMetadata_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role
#define meshtastic_DeviceMetadata_hw_model_ENUMTYPE meshtastic_HardwareModel
#define meshtastic_LoRaPresetGroup_presets_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset
#define meshtastic_LoRaPresetGroup_default_preset_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset
#define meshtastic_LoRaRegionPresets_region_ENUMTYPE meshtastic_Config_LoRaConfig_RegionCode
@@ -1641,6 +1700,9 @@ extern "C" {
#define meshtastic_NeighborInfo_init_default {0, 0, 0, 0, {meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default}}
#define meshtastic_Neighbor_init_default {0, 0, 0, 0}
#define meshtastic_DeviceMetadata_init_default {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0}
#define meshtastic_LoRaPresetGroup_init_default {0, {_meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN}, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0}
#define meshtastic_LoRaRegionPresets_init_default {_meshtastic_Config_LoRaConfig_RegionCode_MIN, 0}
#define meshtastic_LoRaRegionPresetMap_init_default {0, {meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default}, 0, {meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default}}
#define meshtastic_Heartbeat_init_default {0}
#define meshtastic_NodeRemoteHardwarePin_init_default {0, false, meshtastic_RemoteHardwarePin_init_default}
#define meshtastic_ChunkedPayload_init_default {0, 0, 0, {0, {0}}}
@@ -1676,6 +1738,9 @@ extern "C" {
#define meshtastic_NeighborInfo_init_zero {0, 0, 0, 0, {meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero}}
#define meshtastic_Neighbor_init_zero {0, 0, 0, 0}
#define meshtastic_DeviceMetadata_init_zero {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0}
#define meshtastic_LoRaPresetGroup_init_zero {0, {_meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN}, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0}
#define meshtastic_LoRaRegionPresets_init_zero {_meshtastic_Config_LoRaConfig_RegionCode_MIN, 0}
#define meshtastic_LoRaRegionPresetMap_init_zero {0, {meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero}, 0, {meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero}}
#define meshtastic_Heartbeat_init_zero {0}
#define meshtastic_NodeRemoteHardwarePin_init_zero {0, false, meshtastic_RemoteHardwarePin_init_zero}
#define meshtastic_ChunkedPayload_init_zero {0, 0, 0, {0, {0}}}
@@ -1866,6 +1931,13 @@ extern "C" {
#define meshtastic_DeviceMetadata_hasRemoteHardware_tag 10
#define meshtastic_DeviceMetadata_hasPKC_tag 11
#define meshtastic_DeviceMetadata_excluded_modules_tag 12
#define meshtastic_LoRaPresetGroup_presets_tag 1
#define meshtastic_LoRaPresetGroup_default_preset_tag 2
#define meshtastic_LoRaPresetGroup_licensed_only_tag 3
#define meshtastic_LoRaRegionPresets_region_tag 1
#define meshtastic_LoRaRegionPresets_group_index_tag 2
#define meshtastic_LoRaRegionPresetMap_groups_tag 1
#define meshtastic_LoRaRegionPresetMap_region_groups_tag 2
#define meshtastic_FromRadio_id_tag 1
#define meshtastic_FromRadio_packet_tag 2
#define meshtastic_FromRadio_my_info_tag 3
@@ -1884,6 +1956,7 @@ extern "C" {
#define meshtastic_FromRadio_clientNotification_tag 16
#define meshtastic_FromRadio_deviceuiConfig_tag 17
#define meshtastic_FromRadio_lockdown_status_tag 18
#define meshtastic_FromRadio_region_presets_tag 19
#define meshtastic_Heartbeat_nonce_tag 1
#define meshtastic_ToRadio_packet_tag 1
#define meshtastic_ToRadio_want_config_id_tag 3
@@ -2128,7 +2201,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttC
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,fileInfo,fileInfo), 15) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,clientNotification,clientNotification), 16) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfig), 17) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_status,lockdown_status), 18)
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_status,lockdown_status), 18) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_presets), 19)
#define meshtastic_FromRadio_CALLBACK NULL
#define meshtastic_FromRadio_DEFAULT NULL
#define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket
@@ -2146,6 +2220,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_status,lockdown_sta
#define meshtastic_FromRadio_payload_variant_clientNotification_MSGTYPE meshtastic_ClientNotification
#define meshtastic_FromRadio_payload_variant_deviceuiConfig_MSGTYPE meshtastic_DeviceUIConfig
#define meshtastic_FromRadio_payload_variant_lockdown_status_MSGTYPE meshtastic_LockdownStatus
#define meshtastic_FromRadio_payload_variant_region_presets_MSGTYPE meshtastic_LoRaRegionPresetMap
#define meshtastic_LockdownStatus_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, state, 1) \
@@ -2264,6 +2339,27 @@ X(a, STATIC, SINGULAR, UINT32, excluded_modules, 12)
#define meshtastic_DeviceMetadata_CALLBACK NULL
#define meshtastic_DeviceMetadata_DEFAULT NULL
#define meshtastic_LoRaPresetGroup_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, UENUM, presets, 1) \
X(a, STATIC, SINGULAR, UENUM, default_preset, 2) \
X(a, STATIC, SINGULAR, BOOL, licensed_only, 3)
#define meshtastic_LoRaPresetGroup_CALLBACK NULL
#define meshtastic_LoRaPresetGroup_DEFAULT NULL
#define meshtastic_LoRaRegionPresets_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, region, 1) \
X(a, STATIC, SINGULAR, UINT32, group_index, 2)
#define meshtastic_LoRaRegionPresets_CALLBACK NULL
#define meshtastic_LoRaRegionPresets_DEFAULT NULL
#define meshtastic_LoRaRegionPresetMap_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, groups, 1) \
X(a, STATIC, REPEATED, MESSAGE, region_groups, 2)
#define meshtastic_LoRaRegionPresetMap_CALLBACK NULL
#define meshtastic_LoRaRegionPresetMap_DEFAULT NULL
#define meshtastic_LoRaRegionPresetMap_groups_MSGTYPE meshtastic_LoRaPresetGroup
#define meshtastic_LoRaRegionPresetMap_region_groups_MSGTYPE meshtastic_LoRaRegionPresets
#define meshtastic_Heartbeat_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, nonce, 1)
#define meshtastic_Heartbeat_CALLBACK NULL
@@ -2328,6 +2424,9 @@ extern const pb_msgdesc_t meshtastic_Compressed_msg;
extern const pb_msgdesc_t meshtastic_NeighborInfo_msg;
extern const pb_msgdesc_t meshtastic_Neighbor_msg;
extern const pb_msgdesc_t meshtastic_DeviceMetadata_msg;
extern const pb_msgdesc_t meshtastic_LoRaPresetGroup_msg;
extern const pb_msgdesc_t meshtastic_LoRaRegionPresets_msg;
extern const pb_msgdesc_t meshtastic_LoRaRegionPresetMap_msg;
extern const pb_msgdesc_t meshtastic_Heartbeat_msg;
extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePin_msg;
extern const pb_msgdesc_t meshtastic_ChunkedPayload_msg;
@@ -2365,6 +2464,9 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_NeighborInfo_fields &meshtastic_NeighborInfo_msg
#define meshtastic_Neighbor_fields &meshtastic_Neighbor_msg
#define meshtastic_DeviceMetadata_fields &meshtastic_DeviceMetadata_msg
#define meshtastic_LoRaPresetGroup_fields &meshtastic_LoRaPresetGroup_msg
#define meshtastic_LoRaRegionPresets_fields &meshtastic_LoRaRegionPresets_msg
#define meshtastic_LoRaRegionPresetMap_fields &meshtastic_LoRaRegionPresetMap_msg
#define meshtastic_Heartbeat_fields &meshtastic_Heartbeat_msg
#define meshtastic_NodeRemoteHardwarePin_fields &meshtastic_NodeRemoteHardwarePin_msg
#define meshtastic_ChunkedPayload_fields &meshtastic_ChunkedPayload_msg
@@ -2388,6 +2490,9 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_KeyVerificationNumberInform_size 58
#define meshtastic_KeyVerificationNumberRequest_size 52
#define meshtastic_KeyVerification_size 79
#define meshtastic_LoRaPresetGroup_size 26
#define meshtastic_LoRaRegionPresetMap_size 490
#define meshtastic_LoRaRegionPresets_size 5
#define meshtastic_LockdownStatus_size 53
#define meshtastic_LogRecord_size 426
#define meshtastic_LowEntropyKey_size 0
+83
View File
@@ -200,6 +200,87 @@ static void test_applyModemConfig_customCodingRateLowerThanPreset()
TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr());
}
// -----------------------------------------------------------------------
// getRegionPresetMap() — region->valid-preset map sent to clients during want_config
// -----------------------------------------------------------------------
static size_t countKnownRegions()
{
size_t n = 0;
for (const RegionInfo *r = regions; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET; r++)
n++;
return n;
}
// Every region in the firmware table (except the UNSET sentinel) must appear
// exactly once in the map, and all counts must stay within the mesh.options bounds
// (exceeding them would mean nanopb silently truncates the wire message).
static void test_regionPresetMap_coversAllRegionsWithinBounds()
{
meshtastic_LoRaRegionPresetMap map;
getRegionPresetMap(map);
const size_t known = countKnownRegions();
TEST_ASSERT_EQUAL_UINT((unsigned)known, (unsigned)map.region_groups_count);
// Bounds derived from the generated nanopb arrays (mesh.options max_count), so
// this stays correct if those bounds change.
const size_t maxGroups = sizeof(map.groups) / sizeof(map.groups[0]);
const size_t maxRegions = sizeof(map.region_groups) / sizeof(map.region_groups[0]);
TEST_ASSERT_GREATER_THAN_UINT(0, map.groups_count);
TEST_ASSERT_LESS_OR_EQUAL_UINT((unsigned)maxGroups, map.groups_count);
TEST_ASSERT_LESS_OR_EQUAL_UINT((unsigned)maxRegions, map.region_groups_count);
// Each known region appears exactly once.
for (const RegionInfo *r = regions; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET; r++) {
int hits = 0;
for (pb_size_t i = 0; i < map.region_groups_count; i++)
if (map.region_groups[i].region == r->code)
hits++;
TEST_ASSERT_EQUAL_INT(1, hits);
}
}
// The advertised presets must agree with the live region table: every preset is
// legal in its region, the default is among them, and the licensed flag matches.
static void test_regionPresetMap_matchesRegionTable()
{
meshtastic_LoRaRegionPresetMap map;
getRegionPresetMap(map);
for (pb_size_t i = 0; i < map.region_groups_count; i++) {
meshtastic_Config_LoRaConfig_RegionCode code = map.region_groups[i].region;
uint8_t gi = map.region_groups[i].group_index;
TEST_ASSERT_LESS_THAN_UINT(map.groups_count, gi);
const meshtastic_LoRaPresetGroup &grp = map.groups[gi];
const RegionInfo *r = getRegion(code);
// Group's list is non-empty, within the generated array bound, and is the
// region's full list.
const size_t maxPresets = sizeof(grp.presets) / sizeof(grp.presets[0]);
TEST_ASSERT_GREATER_THAN_UINT(0, 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.
for (pb_size_t p = 0; p < grp.presets_count; p++)
TEST_ASSERT_TRUE(r->supportsPreset(grp.presets[p]));
// Default preset matches the table, is legal, and is present in the list.
TEST_ASSERT_EQUAL(r->getDefaultPreset(), grp.default_preset);
TEST_ASSERT_TRUE(r->supportsPreset(grp.default_preset));
bool defaultInList = false;
for (pb_size_t p = 0; p < grp.presets_count; p++)
if (grp.presets[p] == grp.default_preset)
defaultInList = true;
TEST_ASSERT_TRUE(defaultInList);
// Licensed flag matches the region's profile.
TEST_ASSERT_EQUAL(r->profile->licensedOnly, grp.licensed_only);
}
}
void setUp(void)
{
mockMeshService = new MockMeshService();
@@ -241,6 +322,8 @@ void setup()
RUN_TEST(test_applyModemConfig_codingRateMatchesPreset);
RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset);
RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset);
RUN_TEST(test_regionPresetMap_coversAllRegionsWithinBounds);
RUN_TEST(test_regionPresetMap_matchesRegionTable);
exit(UNITY_END());
}