Skip to content

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

Merged
jamesarich merged 11 commits into
mainfrom
claude/admiring-wu-af5cae
Jun 24, 2026
Merged

feat(lora): consume region→preset compatibility map + TINY presets (protobufs #951)#5834
jamesarich merged 11 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

@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.

jamesarich added a commit that referenced this pull request Jun 18, 2026
Rebased onto main (takpacket-sdk 0.5.3 -> 0.7.0, plus compose/lifecycle/room/
detekt bumps). Verified takpacket-sdk 0.7.0 STILL transitively pins
protobufs:2.7.25, so removing the resolutionStrategy force downgrades the test
runtime classpath and breaks region_presets at runtime (84 NoSuchMethodError in
:core:data:allTests). Force retained; refreshed its note to reflect 0.7.0 / #5834.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich force-pushed the claude/admiring-wu-af5cae branch 2 times, most recently from 402edea to a3c08b5 Compare June 18, 2026 22:19
jamesarich added a commit that referenced this pull request Jun 18, 2026
Rebased onto main (takpacket-sdk 0.5.3 -> 0.7.0, plus compose/lifecycle/room/
detekt bumps). Verified takpacket-sdk 0.7.0 STILL transitively pins
protobufs:2.7.25, so removing the resolutionStrategy force downgrades the test
runtime classpath and breaks region_presets at runtime (84 NoSuchMethodError in
:core:data:allTests). Force retained; refreshed its note to reflect 0.7.0 / #5834.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thebentern thebentern marked this pull request as ready for review June 18, 2026 22:19
jamesarich added a commit that referenced this pull request Jun 24, 2026
Rebased onto main (takpacket-sdk 0.5.3 -> 0.7.0, plus compose/lifecycle/room/
detekt bumps). Verified takpacket-sdk 0.7.0 STILL transitively pins
protobufs:2.7.25, so removing the resolutionStrategy force downgrades the test
runtime classpath and breaks region_presets at runtime (84 NoSuchMethodError in
:core:data:allTests). Force retained; refreshed its note to reflect 0.7.0 / #5834.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich force-pushed the claude/admiring-wu-af5cae branch from a3c08b5 to fe6af4f Compare June 24, 2026 17:17
jamesarich and others added 10 commits June 24, 2026 12:45
…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>
Rebased onto main (takpacket-sdk 0.5.3 -> 0.7.0, plus compose/lifecycle/room/
detekt bumps). Verified takpacket-sdk 0.7.0 STILL transitively pins
protobufs:2.7.25, so removing the resolutionStrategy force downgrades the test
runtime classpath and breaks region_presets at runtime (84 NoSuchMethodError in
:core:data:allTests). Force retained; refreshed its note to reflect 0.7.0 / #5834.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
protobufs #959 switched RB snapshot publishing to immutable per-commit
vX.Y.Z-{shorthash}-SNAPSHOT coordinates. Retarget off the moving
develop-SNAPSHOT to the exact published snapshot containing #951.

The takpacket transitive protobufs:2.7.25 force stays: Gradle still ranks
2.7.25 > v2.7.26-…-SNAPSHOT (numeric "2" outranks the "v2" qualifier),
so the runtime classpath would downgrade without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich force-pushed the claude/admiring-wu-af5cae branch from 4c0c30d to 39e82e2 Compare June 24, 2026 17:46
The offline verify manifest hardcoded gradle-9.5.1-all.zip, but the wrapper
moved to 9.6.0 in #5860. captureFlatpakSources mirrors the deps 9.6.0 needs
(incl. the kotlin-dsl plugin marker 6.5.7 bundled with 9.6.0), then the
offline build ran them under 9.5.1, which can't resolve that artifact:

  could not resolve plugin artifact
  'org.gradle.kotlin.kotlin-dsl:...gradle.plugin:6.5.7'

#5860 bumped the wrapper but not this pin. Match it (url + sha256).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich added this pull request to the merge queue Jun 24, 2026
Merged via the queue into main with commit 8fb2001 Jun 24, 2026
25 checks passed
@jamesarich jamesarich deleted the claude/admiring-wu-af5cae branch June 24, 2026 18:53
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