Skip to content

fix(lora): validate 2.4 GHz bandwidth options - #2225

Merged
garthvh merged 6 commits into
meshtastic:mainfrom
RCGV1:codex/2ghz-lora-bandwidth
Aug 13, 2026
Merged

garthvh merged 6 commits into
meshtastic:mainfrom
RCGV1:codex/2ghz-lora-bandwidth

Conversation

@RCGV1

@RCGV1 RCGV1 commented Jul 31, 2026

Copy link
Copy Markdown
Member

What changed?

  • Replaces the global custom-bandwidth list with region- and target-aware choices.
  • Keeps the existing sub-GHz choices and exposes the canonical 2.4 GHz wire values: 200, 400, and 800.
  • Adds 1600 only for firmware targets known to be SX128x-only. Mixed or unknown targets use the conservative set.
  • Preserves an already-stored unsupported value in the picker, explains why it cannot be saved, and blocks the config write until the user selects a supported value.
  • Keeps explicitly persisted 250 kHz sub-GHz configurations compatible with the existing protobuf-default picker value.

Why did it change?

The Apple custom LoRa bandwidth picker only offered sub-GHz values (31/62/125/250/500 kHz), even for the LORA_24 region. Pre-fix firmware accepted and persisted an unsupported value such as 125 kHz before RadioLib rejected it and recorded INVALID_RADIO_SETTING (critical error 7). On display-equipped nodes, firmware focuses the critical-fault screen, which looks like a frozen node even though the scheduler and serial API remain responsive. The saved configuration can also disagree with the bandwidth retained by the radio.

Firmware accepts the canonical high-band codes 200/400/800 on both supported radio families, while 1600 is SX128x-only. tlora-t3s3-v1 is deliberately limited to 200/400/800 because that target can contain either SX128x or LR1121 and the protocol does not currently report the runtime radio chip.

How is this tested?

  • BandwidthsTests: 10 tests passed, including SX128x, LR1121, mixed-target, unknown-region fallback, invalid stored value, and legacy 250 kHz coverage.
  • Related LoRa regression run: 44 tests in 5 suites passed on iPhone 16 Pro / iOS 18.6 Simulator.
  • git diff --check passed.
  • Pre-fix physical Muzi Base/LR1121 and TLORA_T3_S3/SX128x nodes both accepted and repeatedly read back LORA_24 125 kHz; each recorded radio-specific critical error 7 while serial/API probes continued passing for 20 seconds.
  • Source tracing confirmed the apparent freeze is the focused Critical fault #7 display frame, not a stopped CPU or API.
  • Firmware bench validation on a physical SX128x TLORA_T3_S3 (tlora-t3s3-v1) passed 200/400/800 configuration and RF delivery; that specific unit also accepted 1600.
  • An LR1121 Muzi target rejected/normalized 1600 to 800, confirming why mixed and unknown targets must not expose 1600.
  • With the companion firmware fix, an invalid sub-GHz 125 code on LORA_24 is corrected to 800 while the node remains responsive.
  • The exact Apple-commit physical endurance matrix is still pending; these results validate the capability policy and failure mode, not yet the complete app UI/config-write path.

Screenshots/Videos (when applicable)

No visual evidence is needed for the model-layer change. Firmware bench evidence is summarized above; the final exact-commit endurance matrix is still pending.

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated docs/user/settings.md with the filtered bandwidth picker and blocked-save behavior.
  • I have tested the change to ensure that it works as intended in unit and simulator tests.
  • I have completed the exact Apple-app endurance matrix on physical 2.4 GHz hardware.

Summary by CodeRabbit

  • New Features

    • Added LoRa bandwidth options from 200 to 1600 kHz, including explicit 250 kHz support.
    • Bandwidth choices now adapt to the selected region and connected device.
    • Added descriptions, defaults, and guidance for available bandwidth settings.
    • Added warnings when a custom bandwidth is unavailable or requires selection.
  • Bug Fixes

    • Unsupported saved bandwidth values are now identified and marked invalid.
    • Saving is prevented when an unsupported custom bandwidth is selected.
  • Documentation

    • Documented LoRa bandwidth availability, regional limitations, defaults, and hardware compatibility.

Copilot AI review requested due to automatic review settings July 31, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@RCGV1 RCGV1 added the skip-docs-check Use this label to skip the automatic docs audit label Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d37a039-7799-485b-8185-cafe4bd913b9

📥 Commits

Reviewing files that changed from the base of the PR and between 6ba738a and 313c176.

📒 Files selected for processing (8)
  • Localizable.xcstrings
  • Meshtastic/Enums/LoraConfigEnums.swift
  • Meshtastic/Resources/docs/index.json
  • Meshtastic/Resources/docs/markdown/user/settings.md
  • Meshtastic/Resources/docs/user/settings.html
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • MeshtasticTests/LoraDeviceEnumTests.swift
  • docs/user/settings.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/user/settings.md
  • Meshtastic/Resources/docs/user/settings.html
  • Meshtastic/Resources/docs/markdown/user/settings.md
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • MeshtasticTests/LoraDeviceEnumTests.swift

📝 Walkthrough

Walkthrough

The change adds region- and hardware-aware LoRa bandwidth options. The bandwidth enum defines four new values and validates selections by region and hardware type. The configuration UI filters options, preserves unsupported stored values, displays warnings, and blocks invalid saves. Tests cover selection rules, picker mapping, and pioEnv updates.

Changes

Region-aware bandwidth selection

Layer / File(s) Summary
Bandwidth cases and selection rules
Meshtastic/Enums/LoraConfigEnums.swift, MeshtasticTests/LoraDeviceEnumTests.swift
Bandwidths adds 200, 400, 800, and 1600 kHz cases. It filters options by region and hardware, validates stored values, and maps picker values. Tests cover regional filtering, hardware compatibility, validation, and 250 kHz handling.
Custom configuration integration
Meshtastic/Views/Settings/Config/LoRaConfig.swift, Localizable.xcstrings, docs/user/settings.md, Meshtastic/Resources/docs/...
The custom picker uses filtered options, preserves unsupported stored values, displays localized validation messages, and blocks invalid saves. Documentation describes the filtering and validation behavior.
Device environment ingestion validation
MeshtasticTests/MeshPacketsAndTelemetryTests.swift
Tests verify that repeated MyInfo packets update one node record and refresh bandwidth capability from the new pioEnv value.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: 🟡 Moderate · up to 313c1

The bandwidth validation change is not merge-ready until the failing capability assertion is corrected; otherwise the test suite does not reliably verify the supported 2.4 GHz target behavior. The duplicate documentation heading should also be cleaned up before merging.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LoRaConfig
  participant Bandwidths
  participant CustomBandwidthPicker
  User->>LoRaConfig: select custom bandwidth
  LoRaConfig->>Bandwidths: selectable(region, pioEnv)
  Bandwidths-->>LoRaConfig: supported options
  LoRaConfig->>CustomBandwidthPicker: display options and validation state
  User->>LoRaConfig: save configuration
  LoRaConfig->>Bandwidths: validate stored value
  Bandwidths-->>LoRaConfig: validation result
  LoRaConfig-->>User: save or display warning
Loading

Suggested reviewers: garthvh, bruschill

Poem

A rabbit tunes the bandwidth wide,
With region rules and radio guide.
Invalid picks stay in view,
Warnings mark what choices do.
When the setting passes right,
Save it cleanly, hop goodnight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: validation of 2.4 GHz LoRa bandwidth options.
Description check ✅ Passed The description covers the required changes, rationale, testing, documentation, screenshots, and checklist status.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RCGV1

RCGV1 commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
MeshtasticTests/LoraDeviceEnumTests.swift (1)

179-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the nil-region fallback.

Bandwidths.selectable falls back to subGHzCases when region is nil. This branch is reachable in production when RegionCodes(rawValue:) fails to resolve an unrecognized region code (for example, a newer firmware region the app doesn't yet know). Add a test asserting Bandwidths.selectable(region: nil, pioEnv: nil) returns the sub-GHz set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MeshtasticTests/LoraDeviceEnumTests.swift` around lines 179 - 183, Add a test
alongside subGHzOptions_preserveExistingWireValues that calls
Bandwidths.selectable with region: nil and pioEnv: nil, then asserts its
pickerValue list matches the expected sub-GHz set. Keep the assertion consistent
with the existing selectable-options test.
Meshtastic/Views/Settings/Config/LoRaConfig.swift (1)

251-264: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update docs/user/settings.md for this bandwidth picker change.

This changes general LoRa settings behavior: the bandwidth picker now filters options by region/hardware and can block saves on an unsupported value. Update docs/user/settings.md to describe the new behavior.

As per path instructions, "Update docs/user/telemetry.md for telemetry/sensor settings changes and docs/user/settings.md for general settings changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meshtastic/Views/Settings/Config/LoRaConfig.swift` around lines 251 - 264,
Update docs/user/settings.md to document the CustomBandwidthPicker behavior in
the general LoRa settings: available bandwidth options are filtered by region
and hardware, and saving is blocked when the selected value is unsupported. Keep
the documentation aligned with the existing settings terminology and describe
the user-visible behavior without changing unrelated sections.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Meshtastic/Views/Settings/Config/LoRaConfig.swift`:
- Around line 522-551: Update the warning Text inside CustomBandwidthPicker to
apply the existing .localized pattern to the static unsupported-bandwidth
message, matching the neighboring “Licensed band” restriction text while
preserving the current wording and display behavior.

---

Nitpick comments:
In `@Meshtastic/Views/Settings/Config/LoRaConfig.swift`:
- Around line 251-264: Update docs/user/settings.md to document the
CustomBandwidthPicker behavior in the general LoRa settings: available bandwidth
options are filtered by region and hardware, and saving is blocked when the
selected value is unsupported. Keep the documentation aligned with the existing
settings terminology and describe the user-visible behavior without changing
unrelated sections.

In `@MeshtasticTests/LoraDeviceEnumTests.swift`:
- Around line 179-183: Add a test alongside
subGHzOptions_preserveExistingWireValues that calls Bandwidths.selectable with
region: nil and pioEnv: nil, then asserts its pickerValue list matches the
expected sub-GHz set. Keep the assertion consistent with the existing
selectable-options test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd65aa9d-92af-4586-8f1e-fd338e305a11

📥 Commits

Reviewing files that changed from the base of the PR and between de26cc4 and 7e68928.

📒 Files selected for processing (3)
  • Meshtastic/Enums/LoraConfigEnums.swift
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • MeshtasticTests/LoraDeviceEnumTests.swift

Comment thread Meshtastic/Views/Settings/Config/LoRaConfig.swift
Copilot AI review requested due to automatic review settings July 31, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@RCGV1 RCGV1 removed the skip-docs-check Use this label to skip the automatic docs audit label Jul 31, 2026
@RCGV1

RCGV1 commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

RCGV1 commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

Hardware follow-up on the firmware failure mode this UI prevents:

  • Pre-fix Muzi Base/LR1121 and TLORA_T3_S3/SX128x both accepted and persisted custom LORA_24 bandwidth 125.
  • RadioLib rejected the unsupported bandwidth and firmware recorded INVALID_RADIO_SETTING (critical error 7).
  • Both nodes continued answering serial configuration reads for 20 seconds. The apparent freeze comes from firmware focusing the Critical fault #7 display frame; the CPU/API did not stop.
  • The saved config can therefore disagree with the radio's retained internal bandwidth until corrected.

This supports filtering unsupported values in the client: it prevents both the fault-screen experience and the persisted radio/config mismatch. The Apple app itself was not used to write the invalid value during this reproduction. The companion firmware-side validation is in meshtastic/firmware#11317.

@jamesarich

Copy link
Copy Markdown
Contributor

Reviewed this together with meshtastic/Meshtastic-Android#6529 and meshtastic/firmware#11317.

bandwidth = 0 is a valid firmware state, and this flags it as unsupported

isValid(0, region: .lora24, …)pickerValue(forStoredValue: 0, .lora24) returns 0 → not in {200, 400, 800} → Save disabled with an "Unsupported" label.

But 0 is firmware's documented "use the default" sentinel. AdminModule.cpp says "In preset mode bandwidth 0 is expected (the preset supplies it), so leave it untouched", and Default.h says the same about both bandwidth and spread factor. checkOrClampConfigLora only ever assigns loraConfig.bandwidth in its failure branches, so a healthy preset-mode LORA_24 node genuinely reports 0 — and toggling Use Preset off lands on it immediately.

Only affects custom mode, and the user can pick a value to escape, so it isn't a deadlock. But the label is wrong, and it gates the whole Save button — a node persisted with usePreset = false + bandwidth = 0 can't have its hop limit or TX power changed without also picking a bandwidth. Same issue exists on the Android PR.

pioEnv has no local-node guard

LoRaConfig.swift reads node?.myInfo?.pioEnv for whichever node is being configured. Multiple MyInfoEntity rows can coexist — they're fetched unpredicated in AccessoryManager+FromRadio and keyed by peripheralId / myNodeNum elsewhere — so a node that was previously the connected radio keeps its myInfo and its target.

Arguably this is more correct than Android's approach (which nulls pioEnv for any remote destination), since it is that node's own target. But it goes stale across a reflash to a different variant, and the two apps shouldn't disagree on the same policy. Worth aligning one way or the other.

Allowlist

sx128xTargets matches Android's and has the same two omissions vs firmware variants/: my-esp32s3-diy-oled and my-esp32s3-diy-eink. Fail-closed, so cosmetic.

Localization

The warning string is inline English with .localized and isn't added to the string catalog, so it won't be translated. The Android side added a proper string resource.

Also

mergefreeze is currently failing on this PR.

Verified good: Bandwidths.allCases has no consumers outside LoRaConfig.swift and the tests, so widening the enum from 5 to 9 cases is contained.

@RCGV1

RCGV1 commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Addressed the remaining bandwidth review findings in bb96dd0 (after merging current upstream/main):

  • LORA_24 custom mode now treats stored bandwidth 0 as a distinct “choose a bandwidth” state rather than claiming the hardware is unsupported; saving remains blocked until custom mode has a concrete value.
  • Added focused coverage for that 0 state and for both confirmed SX128x-only DIY environments.
  • Verified the existing MyInfo upsert refreshes pioEnv for the same node number after a reflash, and added a regression test proving the persistent node identity is retained while capability selection updates.
  • Added both validation messages to Localizable.xcstrings and updated the settings documentation.

Validation: the changed Swift files parse cleanly, the string catalog passes jq empty, git diff --check passes, and changed-file SwiftLint reports only pre-existing file-size/parameter-count warnings. Focused simulator tests are currently blocked before execution by the same Swift compiler type-check timeout at SecurityConfig.swift:54 on an untouched upstream/main checkout; hosted CI is running on this commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/user/settings.md (1)

181-181: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename the duplicate Tools heading.

Line 181 triggers markdownlint MD024 because another heading has the same content. Use a unique heading so generated documentation has unambiguous navigation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user/settings.md` at line 181, Rename the duplicate “Tools” heading in
the documentation to a unique, descriptive heading while preserving the
section’s content and navigation meaning.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user/settings.md`:
- Line 37: Update the Bandwidth row to distinguish stored value 0 behavior by
region: treat it as 250 kHz in sub-GHz custom mode, while lora24 treats it as
selection required and disables Save until a supported bandwidth is chosen.
Regenerate the bundled Markdown and HTML documentation using the documented
build command.

In `@MeshtasticTests/MeshPacketsAndTelemetryTests.swift`:
- Around line 92-93: Update the positive assertion around mesh.myInfoPacket to
use a pioEnv included in the sx128xTargets allowlist so .sixteenHundred is
selectable; keep my-esp32s3-diy-oled only if it is explicitly added to that
allowlist, and add a separate negative assertion for omitted targets if
fail-closed behavior is intended.

---

Outside diff comments:
In `@docs/user/settings.md`:
- Line 181: Rename the duplicate “Tools” heading in the documentation to a
unique, descriptive heading while preserving the section’s content and
navigation meaning.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0f58679-c0b3-4d51-a52d-81c221f6267a

📥 Commits

Reviewing files that changed from the base of the PR and between 0a67842 and bb96dd0.

📒 Files selected for processing (6)
  • Localizable.xcstrings
  • Meshtastic/Enums/LoraConfigEnums.swift
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • MeshtasticTests/LoraDeviceEnumTests.swift
  • MeshtasticTests/MeshPacketsAndTelemetryTests.swift
  • docs/user/settings.md
💤 Files with no reviewable changes (1)
  • Localizable.xcstrings
🚧 Files skipped from review as they are similar to previous changes (3)
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • MeshtasticTests/LoraDeviceEnumTests.swift
  • Meshtastic/Enums/LoraConfigEnums.swift

Comment thread docs/user/settings.md Outdated
Comment thread MeshtasticTests/MeshPacketsAndTelemetryTests.swift
@RCGV1

RCGV1 commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Updated the protobuf-default bandwidth handling in 313c176. Following existing client patterns for proto scalar defaults, wire value 0 is preserved and displayed as its effective firmware value instead of being treated as missing: Default (812.5 kHz) for LORA_24, while sub-GHz retains its existing 250 kHz interpretation. Unsupported nonzero values remain visible and block saving.

Validation:

  • JSON catalogs/index parse successfully
  • docs bundle regenerated successfully
  • focused simulator build compiled the changed bandwidth code and tests, but test execution was canceled by the existing current-main SecurityConfig.swift compiler type-check timeout

@garthvh
garthvh merged commit 1e9595e into meshtastic:main Aug 13, 2026
5 of 7 checks passed
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.

4 participants