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:
@@ -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.
|
||||
+1
-1
Submodule protobufs updated: 3625166717...f16a13e433
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user