Skip to content

feat(config): filter LoRa modem presets by region legality (#3924 Part 1)#3936

Merged
Yeraze merged 2 commits into
mainfrom
feat/3924-region-preset-filtering
Jul 5, 2026
Merged

feat(config): filter LoRa modem presets by region legality (#3924 Part 1)#3936
Yeraze merged 2 commits into
mainfrom
feat/3924-region-preset-filtering

Conversation

@Yeraze

@Yeraze Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Part 1 of #3924 — per-region modem-preset filtering, the follow-up to the amateur-radio advisory warning (Part 2) merged in #3930. Mirrors the official Meshtastic mobile apps / firmware 2.8 by filtering the LoRa modem-preset picker to only the presets that are legal for the currently selected region. The #3930 warning is untouched; filtering complements it.

Feasibility finding: the "2.8 wire map" does not exist

The original blocker note in constants.ts claimed firmware 2.8 introduces a "region → preset legality map (sent once by the device)" that MeshMonitor couldn't consume because the vendored protobufs (v2.7.26) predate it. That is not accurate.

  • There is NO protobuf wire field carrying a region→preset legality map — not at any 2.8.x tag. Verified against meshtastic/protobufs config.proto: Config.LoRaConfig only exposes region (field 7) and modem_preset (field 2).
  • The official apps instead replicate a firmware-side computation locally. A preset is legal for a region iff at least one channel of the preset's bandwidth fits the region's band: (freqEnd − freqStart) ≥ presetBandwidthKHz / 1000 (spacing is 0 for every current region, so it reduces to span ≥ bandwidth). If the check fails, firmware silently rewrites the preset to LONG_FAST.
  • Firmware sources: src/mesh/RadioInterface.cpp regions[] table (freqStart/freqEnd), src/mesh/MeshRadio.h modemPresetToParams() (preset→bandwidth), and the applyModemConfig / bootstrapLoRaConfigFromPreset fit-check.

So no protobuf submodule bump is possible or needed. This PR replicates the firmware table + math as a local constant (path "b").

Changes

  • constants.ts: add REGION_FREQ_INFO (region→band, transcribed from firmware regions[]), PRESET_BANDWIDTH_KHZ (from modemPresetToParams), and helpers getPresetBandwidthKHz, isPresetLegalForRegion, getLegalPresetOptions. Correct the stale "2.8 wire map" comment.
  • LoRaConfigSection.tsx (device config) and RadioConfigurationSection.tsx (remote admin): filter the preset picker via getLegalPresetOptions(region, modemPreset) — same two components where feat(lora): warn when selecting a licensed amateur-radio region (#3924) #3930 put the warning. The currently-selected preset is always retained so the picker never renders blank for a device already on an out-of-band preset. A small note appears when presets are hidden.
  • en.json: add lora_config.preset_filtered_note.
  • constants.test.ts: cover bandwidth lookup (incl. wide-LoRa and unimplemented-preset fallback), per-region legality, and option filtering.

Practical effect

Only EU_868 (0.25 MHz span) filters anything today — it rejects the two 500 kHz presets (SHORT_TURBO, LONG_TURBO). RU (exactly 0.5 MHz) keeps them. Regions whose exact bounds aren't in the firmware master table (ITU amateur bands, newer EU narrow variants) are treated permissively (all presets legal), since their spans comfortably exceed every preset bandwidth.

Testing

  • Full Vitest suite: success: true, 7995 passed, 0 failed (2639 files).
  • tsc -p tsconfig.server.json --noEmit: no new errors (5 pre-existing TelemetryChart.tsx errors on main).
  • vite build: success.
  • ESLint on changed files: 0 errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n

Part 1 of #3924 (Part 2, the amateur-radio advisory warning, merged in
#3930). Mirrors the official Meshtastic mobile apps by filtering the modem-
preset picker to presets that are legal for the selected LoRa region.

There is NO protobuf wire field carrying a region -> preset legality map;
it never landed at any 2.8.x tag (verified against meshtastic/protobufs
config.proto — Config.LoRaConfig only exposes region + modem_preset). The
apps replicate a firmware-side computation locally: a preset is legal for a
region iff (freqEnd - freqStart) >= presetBandwidthKHz/1000. This replicates
that table + math as a local constant (no protobuf submodule bump needed).

- constants.ts: add REGION_FREQ_INFO (from firmware RadioInterface.cpp
  regions[]), PRESET_BANDWIDTH_KHZ (from MeshRadio.h modemPresetToParams),
  and getPresetBandwidthKHz / isPresetLegalForRegion / getLegalPresetOptions
  helpers. Correct the stale comment that claimed a 2.8 wire map exists.
- LoRaConfigSection.tsx + RadioConfigurationSection.tsx: filter the preset
  picker via getLegalPresetOptions(region, modemPreset), always retaining the
  current selection, plus a note when presets are hidden.
- en.json: add lora_config.preset_filtered_note.
- constants.test.ts: cover bandwidth lookup, per-region legality (EU_868
  rejects the 500 kHz presets, RU allows them, wide bands allow all, unknown
  regions permissive), and option filtering.

In practice only EU_868 (0.25 MHz span) filters anything today, rejecting the
two 500 kHz presets (SHORT_TURBO, LONG_TURBO). Complements the #3930 warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(config): filter LoRa modem presets by region legality

Tasks:

  • Gather context and understand the request
  • Read changed files
  • Analyze implementation
  • Post review feedback

Overall Assessment

This is a well-researched and carefully implemented PR. The feasibility investigation is thorough, the firmware-mirroring math is correct, and the implementation is conservative where it needs to be (permissive fallback for unknown regions). A few observations below.


Code Quality

constants.ts — Core logic is solid.

  • REGION_FREQ_INFO, PRESET_BANDWIDTH_KHZ, and the three helper functions are clean, well-typed, and well-documented with firmware source citations. The wideLora flag on RegionFreqInfo is a nice touch.
  • DEFAULT_PRESET_BW handling for unknown presets (LITE_, NARROW_, TINY_*) is correct per firmware behavior.
  • The getLegalPresetOptions always-retain-current-preset behaviour prevents a blank picker when a device is already on an out-of-band setting — correct safety valve.

LoRaConfigSection.tsx — Correct use of useMemo.

The legalPresetOptions is properly memoized on [region, modemPreset] (line 93–96). Good.

RadioConfigurationSection.tsx — Missing memoization.

legalPresetOptions and hasFilteredPresets are computed inline without useMemo (lines 101–102). This component re-renders on any prop change, so both values are recomputed on every render. For a filtering operation over 13 options this is trivially cheap, but it's inconsistent with how LoRaConfigSection.tsx handles the same computation. Worth making consistent.

// RadioConfigurationSection.tsx line 101-102
const legalPresetOptions = getLegalPresetOptions(region, modemPreset);
const hasFilteredPresets = legalPresetOptions.length < MODEM_PRESET_OPTIONS.length;

RadioConfigurationSection.tsx — Unhardened i18n strings.

Several labels in this component are still hardcoded English strings that pre-date this PR (e.g. "Coding Rate", "Frequency Offset", "Override Frequency (Hz)", "Region", "TX Power", etc.). This is pre-existing debt, not introduced here, but worth noting.


Potential Bugs / Edge Cases

isPresetLegalForRegion — floating-point comparison.

The legality check on line 279:

return spanMHz >= bandwidthMHz;

spanMHz for RU is 869.2 - 868.7 = 0.5 and bandwidthMHz for SHORT_TURBO is 500 / 1000 = 0.5. The test at line 91–94 confirms this is expected to return true. In IEEE 754 double precision, 869.2 - 868.7 should evaluate to exactly 0.5 for these specific values, but this is worth noting. A tiny epsilon guard (e.g. spanMHz >= bandwidthMHz - 1e-9) would make this robust to any future region values that land on an exact boundary.

REGION_MAP vs REGION_OPTIONSITU3_70CMITU2_125CM missing from map.

REGION_OPTIONS covers values 0–37, but REGION_MAP (line 339–348) only goes up to EU_N_868 = 32. The newer entries (33 ITU3_2M, 34 ITU1_70CM, 35 ITU2_70CM, 36 ITU3_70CM, 37 ITU2_125CM) are absent. This is pre-existing debt (the map wasn't updated in the earlier #3927 PR that added those entries), not introduced here, but since this PR touched the same file, it would be good to close the gap.

REGION_FREQ_INFO — UNSET (value 0) defaults to US band.

The comment on line 205 says // UNSET (defaults to US band). The firmware does effectively default to US parameters for unset regions in many code paths, but making the UNSET picker appear to filter like US (26 MHz span, all presets legal) is conservative and safe. If firmware ever changes the UNSET default, this would need updating. A NOTE comment cross-referencing where firmware defines the UNSET fallback would help a future maintainer.


Test Coverage

Coverage is solid for the new helpers:

  • Bandwidth lookup: known presets, wide-LoRa, fallback for unimplemented presets.
  • isPresetLegalForRegion: EU_868 rejects 500 kHz, RU keeps 500 kHz, US/LORA_24 are permissive, unknown region and null/undefined are permissive.
  • getLegalPresetOptions: EU_868 filtering, wide region returns all, illegal-current-preset retained, ordering preserved.

Gap: no test for the region=0 (UNSET) case. Since UNSET maps to US band in REGION_FREQ_INFO, it filters the same as US — all presets legal. A quick assert for isPresetLegalForRegion(0, SHORT_TURBO) === true would document this intent.

Gap: no render/integration test for the UI components. The hasFilteredPresets note and the illegal-current-preset retention are pure logic and covered by unit tests, so this is lower priority.


Performance

No concerns. The filter runs over 13 static options, and the expensive path (LoRaConfigSection) is properly memoized. No issues.


Security

No concerns. This is entirely client-side UI filtering with no auth surface. The note in the PR description correctly explains that the firmware will silently rewrite an illegal preset anyway — so even if the filter were bypassed, no unsafe config would persist.


Minor Nits

  • constants.ts line 204: REGION_FREQ_INFO is const but not exported. That's intentional (no external consumers), which is fine — the helpers are the public API.
  • The wideLora?: boolean optional field on RegionFreqInfo (line 199) is slightly surprising: only LORA_24 (value 13) sets it. If a future region also uses the 2.4 GHz band, a developer would need to remember to set this flag. A comment pointing to how it's consumed in getPresetBandwidthKHz would help.

Summary

The core logic is correct, well-tested, and correctly mirrors firmware behavior. The two actionable items for this PR are:

  1. Floating-point boundary guard in isPresetLegalForRegion for robustness.
  2. useMemo in RadioConfigurationSection for consistency with LoRaConfigSection.

The REGION_MAP gap (missing values 33–37) is pre-existing debt; fixing it in this PR would be a welcome cleanup but isn't blocking.

Address review on #3936: use `spanMHz >= bandwidthMHz - 1e-9` so IEEE-754
subtraction (e.g. 869.2 - 868.7) can't mis-exclude a preset whose bandwidth
lands exactly on a region's span. No behavior change for current region values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
@Yeraze

Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

Addressed in 7eca6bd: added an epsilon guard (spanMHz >= bandwidthMHz - 1e-9) so IEEE-754 subtraction can't mis-exclude a preset that lands exactly on a region's span boundary. No behavior change for current region values; the RU/SHORT_TURBO boundary test still passes. The REGION_MAP 33–37 gap is pre-existing debt and left out of scope for this PR.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(config): filter LoRa modem presets by region legality

Tasks:

  • Gather context and understand the request
  • Read changed files
  • Analyze implementation
  • Post review feedback

Updated Review (post 7eca6bd)

This review reflects the latest commit. The previous review's top two action items have been resolved or acknowledged:

Resolved: Epsilon guard ✅

isPresetLegalForRegion (constants.ts:281) now uses:

return spanMHz >= bandwidthMHz - 1e-9;

The guard is correct — 1e-9 is comfortably below any plausible floating-point error from subtraction of MHz values in the hundreds, while being far smaller than the gap between any two distinct bandwidth values (125 kHz = 0.125 MHz). The RU/SHORT_TURBO boundary case at exactly 0.5 MHz is now robust.

Unresolved: useMemo inconsistency in RadioConfigurationSection

LoRaConfigSection.tsx:93–96 memoizes the computation; RadioConfigurationSection.tsx:101–102 does not. The computation itself is trivially cheap (filter over 13 items), so this is a code-style consistency gap only, not a performance issue. Worth noting but not blocking.


Remaining observations

Test gap — region=0 (UNSET) case:

The test suite covers US (1), EU_868 (3), RU (9), LORA_24 (13), ITU3_2M (33), null, and undefined — but not 0 (UNSET). Since REGION_FREQ_INFO[0] maps to the US band, isPresetLegalForRegion(0, anyPreset) will return true for all presets. A quick assertion would document the explicit intent that UNSET is permissive:

expect(isPresetLegalForRegion(0, SHORT_TURBO)).toBe(true); // UNSET -> US band -> all presets legal

This is low priority but would complete coverage of all meaningful input categories.

REGION_FREQ_INFO is not exported:

This is intentional (the helpers are the public API), and noted in the previous review. Still agree with the design — no action needed.

wideLora?: boolean on RegionFreqInfo (line 199-200):

Only LORA_24 (value 13) sets this flag. The type annotation doesn't indicate it's single-use, so a future developer adding a hypothetical second 2.4 GHz band variant would need to know to set it. The comment at line 200 ("True for the 2.4 GHz wide-LoRa band") is sufficient but a cross-reference to getPresetBandwidthKHz where it's consumed would help discoverability. Minor nit.


Overall Assessment

This is ready to merge. The core logic correctly mirrors firmware behavior, the epsilon guard correctly handles the RU boundary, and the permissive fallback for unknown regions is the right conservative choice. The one remaining action item (useMemo consistency) is a style nit only.
| Branch: feat/3924-region-preset-filtering

@Yeraze Yeraze merged commit 35a536e into main Jul 5, 2026
20 checks passed
@Yeraze Yeraze deleted the feat/3924-region-preset-filtering branch July 5, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant