Skip to content

feat(lora): consume region→preset compatibility map + TINY presets (protobufs #951)#5834

Draft
jamesarich wants to merge 9 commits into
mainfrom
claude/admiring-wu-af5cae
Draft

feat(lora): consume region→preset compatibility map + TINY presets (protobufs #951)#5834
jamesarich wants to merge 9 commits into
mainfrom
claude/admiring-wu-af5cae

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

Firmware now advertises which modem presets are legal in each LoRa region during the config handshake (protobufs#951 / firmware#10736). This wires the Android client to consume that map so the preset picker is constrained per region, amateur-only presets are gated, and the two new TINY presets are handled — preventing users from selecting an illegal region/preset combination.

⚠️ Preview / do not merge yet. This tracks org.meshtastic:protobufs:develop-SNAPSHOT to pick up #951, which isn't in a tagged release. Re-pin to a released protobufs and drop the resolution force (see Chores) once #951 ships in a tag.

🌟 New Features

  • Decode and persist FromRadio.region_presets (LoRaRegionPresetMap) as session-scoped state, mirroring the deviceUIConfig handshake lifecycle (repository flow + handler dispatch + cleared on each new handshake).
  • The LoRa config screen filters the modem-preset dropdown to the presets legal in the selected region; changing region snaps an illegal current preset to the region's default_preset.
  • Licensed-only regions (amateur bands) render their presets greyed/non-selectable unless the device is a licensed operator, with an explanatory summary.
  • Add the two new amateur-radio modem presets TINY_FAST / TINY_SLOW (15.625 kHz).
  • Gracefully falls back to the full unconstrained preset list when the firmware predates this message (< 2.8) or a region has no map entry.

🛠️ Refactoring & Architecture

  • Region→preset decoding lives in a pure core/model helper (RegionPresetConstraint), unit-testable off-Compose; the UI stays declarative.
  • DropDownItem gains a defaulted enabled flag so individual menu items can render disabled.
  • A firmware-applied region change (e.g. EU sibling swap — applied live, no reboot) is reflected when the LoRa screen is re-opened; the LoRa write is no longer applied optimistically, so the form never shows a requested-but-not-applied region.

🧹 Chores

  • Track org.meshtastic:protobufs:develop-SNAPSHOT and force the transitive takpacket-sdk protobufs pin to match (root build.gradle.kts). Preview-only — revert with the re-pin above.

Testing Performed

  • LoRaRegionPresetsTest (new): decode + repair + gate — null map / absent region / out-of-range index / empty group → unconstrained; legal-kept / illegal→default / default-fallback; licensed gate on/off.
  • FromRadioPacketHandlerImplTest: region_presets routes to the handler.
  • MeshConfigFlowManagerImplTest: region map cleared on new handshake.
  • MeshConfigHandlerImplTest: handler→repository delegation.
  • RadioConfigViewModelTest: map flow→state, localIsLicensed from the node, and LoRa setConfig is not applied optimistically / issues no re-read.
  • ChannelOptionTest stays green (TINY presets mapped). Full allTests + test + detekt + spotlessCheck + assembleDebug pass locally.

🤖 Generated with Claude Code

jamesarich and others added 8 commits June 17, 2026 12:35
…OW presets

Bump org.meshtastic:protobufs to develop-SNAPSHOT to pick up the LoRa
region->preset map (protobufs #951) and the two new amateur-radio modem
presets. Replays the preview wiring from #5790:

- libs.versions.toml -> develop-SNAPSHOT; root build.gradle.kts forces all
  org.meshtastic:protobufs* to the snapshot so takpacket-sdk's transitive
  protobufs:2.7.25 pin can't downgrade the test runtime classpath
  (assembleDebug/detekt miss it; test/allTests catch it).
- ChannelOption: TINY_FAST/TINY_SLOW at the firmware-accurate 15.625 kHz
  (0.015625f); the proto's "20kHz" is padded channel spacing.
- Channel.kt: TinyFast/TinySlow names + interop-critical doc.
- ModelExtensions.labelRes + label_tiny_fast/label_tiny_slow strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add session-scoped storage for FromRadio.region_presets, mirroring the
deviceUIConfig lifecycle:

- RadioConfigRepository: loraRegionPresetMapFlow + set/clear.
- MeshConfigHandler.handleRegionPresets -> repository setter.
- FromRadioPacketHandlerImpl: dispatch region_presets to the handler.
- MeshConfigFlowManagerImpl: clear the map at each handshake start.
- FakeRadioConfigRepository + FakeMeshConfigHandler: test-double parity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- core/model/RegionPresetConstraint.kt: pure helpers over LoRaRegionPresetMap —
  constraintFor(region) (null = unconstrained for absent map/region/OOB index),
  RegionPresetConstraint.isGated(isLicensed), and repairPresetFor() to snap an
  illegal preset to the region default. Unit-tested in LoRaRegionPresetsTest.
- Channel.kt: add the firmware-exact TinyFast/TinySlow names to the interop
  when() (verified against firmware DisplayFormatters::getModemPresetDisplayName).
- FromRadioPacketHandlerImplTest: assert region_presets dispatches to the handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… re-read after swap

Consume the region->preset map in the LoRa config UI:
- RadioConfigViewModel exposes the decoded map + localIsLicensed (from the
  destination node User.is_licensed, which is reliably populated — unlike
  userConfig, which is empty on the LoRa screen).
- LoRaConfigItemList filters the preset dropdown to the region legal presets,
  snaps an illegal current preset to the region default on region change, and
  disables licensed-only presets (with an explanatory summary) unless the device
  is a licensed operator. Constraints apply to the local device only.
- DropDownItem gains a defaulted enabled flag so individual presets render
  greyed/non-selectable.
- R9: a local LoRa write applies live (no reboot) and may be region-swapped by
  the firmware, so the set ACK now triggers a LoRa re-read to reflect it.
- Falls back to the full unconstrained list when the map is absent (firmware < 2.8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…review auto-fix)

- constraintFor treats an empty-presets group as unconstrained (degrade to the
  full list rather than an empty picker), matching the null/absent/OOB cases.
- LoRaConfigItemList always keeps the current selection in the dropdown
  (disabled when illegal) so the field is never blank, and memoizes the item
  list. Drop the redundant use_preset guard already implied by the enclosing if.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nfig (review auto-fix)

Rename setLoRaRegionPresetMap/clearLoRaRegionPresetMap to
setLoraRegionPresetMap/clearLoraRegionPresetMap for casing parity with the
loraRegionPresetMapFlow property and the existing setLoraConfig convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion (review auto-fix)

- Point the licensed-region summary at the real UI labels (Licensed amateur
  radio (Ham) in User Config) instead of invented ones.
- Add tests for the handshake clear of the region-preset map and the
  handleRegionPresets -> repository delegation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…place (review triage)

Revert the post-save LoRa re-read: it suppressed the normal save-success dialog
and auto-back for local LoRa saves. A firmware region swap (e.g. EU sibling) is
instead reflected when the LoRa screen is next opened, which already re-reads the
device config. Also stop applying the LoRa write optimistically so the form never
shows a requested-but-not-applied region. Local LoRa saves now behave like every
other config save.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📄 Docs staleness check — advisory

This PR modifies user-facing UI source files but does not update any page under docs/en/user/ or docs/en/developer/.

⚠️ Doc changes propagate to 3 consumers: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in docs/en/ automatically flows to all three.

Changed source files:

core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt

What to check:

Changed area Likely doc page
feature/messaging/ docs/en/user/messages-and-channels.md
feature/node/ docs/en/user/nodes.md or docs/en/user/node-metrics.md
feature/map/ docs/en/user/map-and-waypoints.md
feature/connections/ docs/en/user/connections.md
feature/settings/ docs/en/user/settings-radio-user.md or docs/en/user/settings-module-admin.md
feature/firmware/ docs/en/user/firmware.md
feature/intro/ docs/en/user/onboarding.md
feature/discovery/ docs/en/user/discovery.md
feature/docs/ Internal docs infrastructure
core/ui/ docs/en/developer/codebase.md or component-specific user pages

New page checklist (if adding a new doc page):

  1. Create the .md file in docs/en/user/ or docs/en/developer/ with last_updated frontmatter
  2. Register in DocBundleLoader.kt with string resources (in-app browser)
  3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this check.

Cross-platform note: This check is advisory while doc coverage matures. Both Android and Apple repos use the same skip-docs-check label and advisory severity. See meshtastic/design standards for shared conventions.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🖼️ Preview staleness check — advisory

This PR modifies UI composables but does not update any *Previews.kt files.

Previews power screenshot tests and in-app docs screenshots. Keeping them current ensures visual regression coverage stays accurate.

Changed UI files:

feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt

What to check:

Pattern Preview file convention
feature/{name}/…/ui/ or component/ feature/{name}/…/*Previews.kt
core/ui/…/ core/ui/…/ (previews colocated)

Adding previews checklist:

  1. Create or update a *Previews.kt file in the same module with @PreviewLightDark
  2. Add @Suppress("PreviewPublic") if the preview is consumed by screenshot-tests
  3. Add corresponding @PreviewTest function in screenshot-tests/src/screenshotTest/
  4. Run ./gradlew :screenshot-tests:updateDebugScreenshotTest to generate reference images

If this PR does not require preview updates (e.g., logic-only change, non-visual refactor), add the skip-preview-check label to dismiss.

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2910 1 2909 0
View the top 1 failed test(s) by shortest run time
org.meshtastic.core.data.repository.DeviceLinkRepositoryImplTest::reconcilePrunesShortCodesNoLongerInCatalog()[jvm]
Stack Traces | 0.122s run time
org.opentest4j.AssertionFailedError: expected: <[a]> but was: <[a, b]>
	at kotlin.test.junit5.JUnit5Asserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.core.data.repository.DeviceLinkRepositoryImplTest$reconcilePrunesShortCodesNoLongerInCatalog$1.invokeSuspend(DeviceLinkRepositoryImplTest.kt:172)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:278)
	at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWithInternal(DispatchedContinuation.kt:384)
	at kotlinx.coroutines.DispatchedCoroutine.afterResume(Builders.common.kt:588)
	at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants