Skip to content

feat(lora): warn when selecting a licensed amateur-radio region (#3924)#3930

Merged
Yeraze merged 1 commit into
mainfrom
feat/3924-amateur-band-warning
Jul 5, 2026
Merged

feat(lora): warn when selecting a licensed amateur-radio region (#3924)#3930
Yeraze merged 1 commit into
mainfrom
feat/3924-amateur-band-warning

Conversation

@Yeraze

@Yeraze Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #3924

Summary

Issue #3924 asks MeshMonitor to warn on illegal LoRa preset/region combinations, mirroring firmware 2.8 / the official mobile apps. The full behavior the issue describes has two parts:

  1. Filter/flag illegal preset choices per region — this depends on firmware 2.8's region→preset legality map (mesh.proto, "Sent once" by the device). The vendored protobuf submodule is currently v2.7.26, which predates that map, so there is no wire message to consume yet. This part is deferred until the protobuf submodule is bumped to a 2.8 release.
  2. Warn when a selected preset lands in a licensed amateur radio band — this can be done today and is what this PR implements. The ITU RegionCode entries (ITU1_2M = 27, ITU23_2M = 28) map onto the 2m amateur band (144–148 MHz), which is licensed amateur spectrum in every ITU region.

What changed

  • configuration/constants.ts: new AMATEUR_RADIO_REGIONS map + isAmateurRadioRegion() helper (null/undefined-safe). Doc comment records the firmware-2.8 legality-map follow-up.
  • LoRaConfigSection.tsx (device Configuration tab): shows a role="alert" warning banner beneath the Region selector when an amateur band is selected.
  • RadioConfigurationSection.tsx (Remote Admin radio config): same warning banner for parity.
  • public/locales/en.json: new lora_config.amateur_band_warning string (other locales fall back to English via Weblate).
  • LoRaConfigSection.test.tsx: 6 new unit tests for isAmateurRadioRegion (positive ITU cases, negative ISM/SRD/UNSET/out-of-range cases, null/undefined handling, and REGION_MAP sync).

The warning is advisory only — it does not block saving, matching the non-blocking nature of the mobile-app behavior and avoiding false positives while the firmware legality map is unavailable.

Testing

  • Full Vitest suite: success=true, 0 failed suites, 0 failed tests (7947 passed).
  • npm run build: passes.
  • eslint on changed files: 0 errors (3 pre-existing any warnings on untouched prop types).

Follow-up

When the protobuf submodule is bumped to a firmware-2.8 release, the static AMATEUR_RADIO_REGIONS list can be replaced/augmented by the device-reported region→preset legality map to also filter the modem-preset picker and auto-switch to the region default, completing the rest of #3924.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n

Adds a compliance warning to the LoRa configuration UI (both the device
Configuration tab and the Remote Admin radio-config panel) when the
selected RegionCode is one of the ITU 2m amateur-radio bands (ITU1_2M=27,
ITU23_2M=28). Operating on these regions legally requires a valid amateur
radio license, so the banner reminds the operator of their compliance
responsibility — mirroring the amateur-band warning in the official
Meshtastic mobile apps.

New `isAmateurRadioRegion()` helper + `AMATEUR_RADIO_REGIONS` map in
configuration/constants.ts, unit-tested in LoRaConfigSection.test.tsx.

Note: firmware 2.8's region->preset legality map (used by the mobile apps
to filter the preset picker) cannot be consumed yet because the vendored
protobufs are at v2.7.26 and predate it. Per-preset legality filtering is
deferred until the protobuf submodule is bumped to a 2.8 release; the
helper's doc comment records the follow-up.

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(lora): warn when selecting a licensed amateur-radio region

Tasks:

  • Read PR context
  • Review constants.ts changes
  • Review LoRaConfigSection.tsx changes
  • Review RadioConfigurationSection.tsx changes
  • Review LoRaConfigSection.test.tsx changes
  • Review en.json changes
  • Post final review

Code Review

This is a clean, well-scoped implementation. The PR does exactly what it says and nothing more. Here's a breakdown:

constants.ts

Strengths:

  • AMATEUR_RADIO_REGIONS as a Record<number, string> is the right structure — O(1) lookup and can be iterated when needed.
  • isAmateurRadioRegion uses Object.prototype.hasOwnProperty.call() correctly to avoid prototype chain issues — better than region in AMATEUR_RADIO_REGIONS or AMATEUR_RADIO_REGIONS[region] !== undefined.
  • The null/undefined guard is explicit and correct.
  • Excellent doc comments explaining the protobuf submodule limitation and follow-up path.

Minor observations:

  • AMATEUR_RADIO_REGIONS could alternatively be a Set<number> since the name strings aren't used at runtime (only in tests to cross-check against REGION_MAP). Either approach is fine; the current Record is slightly more self-documenting with the names inline.
  • No issues.

LoRaConfigSection.tsx

Strengths:

  • role="alert" on the warning div is correct ARIA — screen readers will announce it when it appears.
  • CSS fallback var(--ctp-yellow, #f9e2af) is thoughtful for environments where the CSS variable may not be defined.
  • Warning is placed directly beneath the region <select> — intuitive UX placement.

Observations:

  • The RadioConfigurationSection.tsx warning (line 224) uses var(--ctp-yellow) without a fallback, while LoRaConfigSection.tsx uses var(--ctp-yellow, #f9e2af). Minor inconsistency — both will work in the Catppuccin theme, but the fallback in LoRaConfigSection.tsx is the safer pattern.

RadioConfigurationSection.tsx

Strengths:

  • Parity with the device configuration view is correct — both surfaces where region is configurable now warn.

Pre-existing issues (not introduced by this PR):

  • Several labels in this component use hardcoded English strings (e.g. "Coding Rate", "Frequency Offset", "Hop Limit (1-7)", "TX Power") rather than t() calls. The region label ("Region") on line 203 is also hardcoded. These are pre-existing i18n gaps unrelated to this PR but worth a follow-up issue.

LoRaConfigSection.test.tsx

Strengths:

  • 6 new tests covering all the important cases: positive (27, 28), negative (ISM regions, UNSET, out-of-range), and edge cases (null, undefined).
  • The REGION_MAP sync test at line 146–151 is excellent — it ensures that if someone adds a new amateur region to AMATEUR_RADIO_REGIONS they must also update REGION_MAP or the test fails.
  • Tests are pure unit tests on the helper function, so no DOM mocking required. Appropriate scope.

Observation:

  • The test file name (LoRaConfigSection.test.tsx) tests constants, not the React component. The file already had this structure before this PR, so this is just a pre-existing naming quirk, not a problem introduced here.

en.json

Strengths:

  • Warning text is accurate and complete: mentions the license requirement, compliance responsibility, and the relevant specifics (band-plan, power, station identification).

Minor suggestion:

  • The string is fairly long. It will work fine, but consider whether a shorter version would be clearer in the UI. This is purely subjective — the current wording is legally appropriate.

Summary

This PR is ready to merge. The implementation is correct, well-tested, and appropriately scoped. The advisory-only approach (no save blocking) is the right call given the absent firmware legality map. The one minor inconsistency (missing CSS variable fallback in RadioConfigurationSection.tsx line 224) is trivial enough that it doesn't warrant blocking the PR.

The two items worth noting as follow-ups (not blocking):

  1. The missing CSS fallback in RadioConfigurationSection.tsx: var(--ctp-yellow)var(--ctp-yellow, #f9e2af) for parity with LoRaConfigSection.tsx.
  2. The pre-existing i18n gaps in RadioConfigurationSection.tsx (hardcoded English labels) could be addressed in a future i18n pass.

@Yeraze Yeraze merged commit 51a5370 into main Jul 5, 2026
19 checks passed
@Yeraze Yeraze deleted the feat/3924-amateur-band-warning branch July 5, 2026 16:04
Yeraze added a commit that referenced this pull request Jul 5, 2026
…t 1) (#3936)

* feat(config): filter LoRa modem presets by region legality (#3924)

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

* fix(lora): epsilon-guard preset legality float compare

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Feature: warn on illegal LoRa preset/region combinations

1 participant