fix(settings): restore channels from device profiles - #6618
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughProfile installation now validates optional channel URLs before radio edits, normalizes channel settings into replacement writes, applies LoRa configuration, and updates local channel state when applicable. Shared utilities also support channel imports and malformed-URL handling. ChangesProfile installation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant RadioConfigViewModel
participant InstallProfileUseCase
participant RadioEditTransaction
RadioConfigViewModel->>InstallProfileUseCase: provide profile and current LoRa context
InstallProfileUseCase->>InstallProfileUseCase: parse and validate channel URL
InstallProfileUseCase->>RadioEditTransaction: write channel replacements and LoRa configuration
RadioEditTransaction-->>InstallProfileUseCase: complete installation
InstallProfileUseCase-->>RadioConfigViewModel: return or raise malformed URL
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
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. Comment |
7a18a02 to
b53194a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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
`@core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt`:
- Around line 132-147: Update ChannelSet.toChannelReplacements() to require
settings.isNotEmpty() in addition to the existing maximum-count validation, so
an empty decoded channel set is rejected before replacement processing or its
transaction begins. Add a regression test covering an empty ChannelSet.settings
input and verify it fails without removing the destination primary channel.
In
`@core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt`:
- Around line 150-167: Extend FakeRadioController to record channel-write and
setConfig operations, then update the test to assert every channel write occurs
before setConfig(Config(lora = loraConfig)). Keep the existing final-state
assertions and use the recorded operation sequence to verify ordering.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 565e64a0-10fc-455a-96ae-08e5fa0d2a90
📒 Files selected for processing (2)
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.ktcore/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt
b53194a to
7071a69
Compare
jamesarich
left a comment
There was a problem hiding this comment.
The gap you're closing is real — device profiles genuinely should restore channels — and decoding the URL before opening the admin transaction is the right call, since it avoids leaving a half-open admin session. The import dialog's channels checkbox is also correctly honored (EditDeviceProfileDialog.kt:97 nulls channel_url when unchecked), so there's no silent wipe path. Four things to address before merge.
Blocking: the new rejection path is silently swallowed
RadioConfigViewModel.installProfile (feature/settings/.../RadioConfigViewModel.kt:725) calls safeLaunch(tag = "installProfile") with no errorEvents, and safeLaunch ends in errorEvents?.tryEmit(message) — a no-op when null (core/ui/.../ViewModelExtensions.kt:136).
So this PR's new "reject malformed URLs before applying any field" behavior lands, from the user's side, as: nothing applied, no error shown, dialog closes as if it succeeded. And because MalformedMeshtasticUrlException isn't an expected-condition label, safeLaunch logs it via Logger.e, filing a Crashlytics/Datadog defect for what is ordinary bad user input. That's strictly worse than the current behavior, where a profile with a bad URL at least installs everything else. This needs a UiText error surfaced to the user (pass an errorEvents flow, or show a snackbar) before the fail-closed behavior is an improvement.
Please reuse the canonical import path rather than re-deriving it
core/ui/.../util/ProtoExtensions.kt already implements this exact operation: importChannelSet (:221), getChannelReplacementList (:122), normalizeReplacementSettings (:168), and a CHANNEL_REPLACEMENT_SLOT_COUNT = 8 constant (:44). This PR adds a second constant of the same name and a hand-rolled toChannelReplacements(). I realize :core:domain can't depend on :core:ui — so the fix is to hoist those helpers down into :core:model/:core:domain and have both call sites share them, not to maintain a parallel copy.
That matters concretely because of what the copy leaves out:
normalizeReplacementSettingsis skipped. Its own docs (ProtoExtensions.kt:152-166) explain that blank placeholder secondaries and semantic duplicates produce invalid LongFast-looking slots that the radio rejects withQueueStatus res=6/routeErr=6. A profile'schannel_urlis generated from the source radio's live channel set (RadioConfigRepositoryImpl.kt:184), which can carry exactly that padding — and this path writes it straight through.- The local channel cache is left stale. Transactional
setChanneldeliberately doesn't mirror to the cache (AdminControllerImpl.kt:238), which is whyimportChannelSetcallsreplaceAllSettingsafter commit.InstallProfileUseCasewrites all 8 slots through that scope and does nothing afterward, so a local profile install leaves the app's channel list wrong until the radio re-sends its config. (Fine for a remotedestNum;installProfilecan be either.)FakeRadioControllerrecords the writes, so no current test can catch this.
Minor
- LoRa config is written twice per install:
installConfigwritesprofile.config.lora, then the channel-set path writeschannelSet.lora_config.importChannelSetguards this withtakeIf { it != currentLoraConfig }to avoid a redundant reconfigure; here, on divergence, the URL silently wins. - A present-but-empty
channel_url(plausible from a non-Android exporter) throws and aborts the entire install.takeIf { it.isNotBlank() }would degrade more gracefully.
Also note CodeRabbit reports "Review skipped: incremental reviews are disabled" on this PR, so your latest push hasn't had a bot pass — worth commenting @coderabbitai full review once you push the fixes.
7071a69 to
69b1681
Compare
|
Thanks — addressed the requested changes in @coderabbitai full review |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== PR metadata ==='
gh pr view 6618 --repo meshtastic/Meshtastic-Android --json number,title,baseRefName,headRefName,commits,files \
--jq '{number, title, baseRefName, headRefName, commits: [.commits[] | {oid: .oid, messageHeadline: .messageHeadline}], files: [.files[] | {path, additions, deletions}]}'
printf '%s\n' '=== Diff stat ==='
gh pr diff 6618 --repo meshtastic/Meshtastic-Android --stat
printf '%s\n' '=== Changed Kotlin source outlines ==='
git diff --name-only "$(git merge-base HEAD origin/HEAD)" HEAD 2>/dev/null || true
fd -e kt . | rg '(^|/)(InstallProfileUseCase|.*Channel.*|.*Profile.*)\.kt$' | while IFS= read -r file; do
echo "--- $file"
ast-grep outline "$file" --items all --type class,function,object,interface --view compact || true
doneLength of output: 3463 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
printf '%s\n' '=== Changed files ==='
git diff --name-status "$base" HEAD
printf '%s\n' '=== Complete patch ==='
git diff --find-renames --find-copies --no-ext-diff "$base" HEAD -- \
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt \
core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt \
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt \
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt \
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/MalformedMeshtasticUrlException.kt \
core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetReplacementTest.kt \
core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt \
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt \
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt \
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt \
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
printf '%s\n' '=== Changed production-file outlines ==='
for file in \
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt \
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt \
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt \
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/MalformedMeshtasticUrlException.kt \
core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt \
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt \
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
do
echo "--- $file"
ast-grep outline "$file" --items all --view compact || true
done
printf '%s\n' '=== Planner and malformed-URL usage ==='
rg -n -C 4 \
-e '\bChannelSetReplacement\b' \
-e '\bMalformedMeshtasticUrlException\b' \
-e '\breplaceAllChannels\b' \
. -g '*.kt'Length of output: 88333 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== InstallProfileUseCase construction and invocation ==='
rg -n -C 5 \
-e '\bInstallProfileUseCase\s*\(' \
-e '\binstallProfileUseCase\s*\(' \
-e '\bInstallProfileUseCase\b' \
. -g '*.kt'
printf '%s\n' '=== Radio edit transaction contracts ==='
rg -n -C 12 \
-e 'interface\s+AdminEditScope\b' \
-e 'fun\s+.*editSettings\s*\(' \
-e 'suspend\s+fun\s+.*editSettings\s*\(' \
-e 'class\s+.*EditSettings' \
-e 'override\s+suspend\s+fun\s+setChannel\s*\(' \
. -g '*.kt'
printf '%s\n' '=== Radio config repository contracts ==='
rg -n -C 10 \
-e 'interface\s+RadioConfigRepository\b' \
-e 'fun\s+replaceAllSettings\s*\(' \
-e 'suspend\s+fun\s+replaceAllSettings\s*\(' \
-e 'class\s+FakeRadioConfigRepository\b' \
. -g '*.kt'
printf '%s\n' '=== Module build dependencies ==='
fd -a -g 'build.gradle.kts' core | while IFS= read -r file; do
if rg -q 'core:common|core:model|core:repository|core:domain' "$file"; then
echo "--- $file"
rg -n -C 3 'core:common|core:model|core:repository|core:domain' "$file"
fi
done
printf '%s\n' '=== Current LoRa state construction and destination selection ==='
rg -n -C 8 \
-e '\bradioConfigState\b' \
-e '\bdestNum\b' \
-e '\bmyNodeNum\b' \
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.ktLength of output: 95566
The shared replacement plan is used by QR imports and profile installation. Profile installation validates the URL before it opens the edit transaction. It writes the channel slots and LoRa configuration in the same transaction. The local cache update occurs only after a successful local transaction. ✏️ Learnings added
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt (1)
21-22: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAssert channel settings on recorded writes.
import_writes_all_eight_slots_with_replacement_roleschecks indices, roles, and cache state, but not the settings sent bysetChannel. Assert imported channel identities at indices 0 and 1 and empty settings for disabled slots. Keep the helper imports for their direct unit tests; they do not construct expected values forimportChannelSet.🤖 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 `@core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt` around lines 21 - 22, Update import_writes_all_eight_slots_with_replacement_roles to inspect the recorded setChannel writes, asserting the imported channel identities at indices 0 and 1 and empty settings for disabled slots. Keep getChannelReplacementList and normalizeReplacementSettings imports for their existing direct unit tests, rather than using them to build importChannelSet expectations.Sources: Coding guidelines, Learnings
🤖 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
`@core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt`:
- Around line 85-87: The local install path in InstallProfileUseCase must
persist normalized settings and the applied LoRa configuration atomically, while
preserving cached LoRa when this install does not write LoRa; update
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt
lines 85-87 to use the repository’s combined atomic operation. In
core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt
lines 136-197, assert URL LoRa takes precedence and replaces cached
ChannelSet.lora_config; in lines 199-213, seed cached LoRa and assert blank URL
plus profile LoRa replaces it.
---
Nitpick comments:
In
`@core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt`:
- Around line 21-22: Update import_writes_all_eight_slots_with_replacement_roles
to inspect the recorded setChannel writes, asserting the imported channel
identities at indices 0 and 1 and empty settings for disabled slots. Keep
getChannelReplacementList and normalizeReplacementSettings imports for their
existing direct unit tests, rather than using them to build importChannelSet
expectations.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a7cc20e5-9240-4f84-aaa8-74550e7cd173
📒 Files selected for processing (11)
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.ktcore/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/util/MalformedMeshtasticUrlException.ktcore/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetReplacementTest.ktcore/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.ktcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.ktcore/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.ktfeature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.ktfeature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
69b1681 to
94d4a4d
Compare
|
The cache-refresh follow-up is now addressed in @coderabbitai full review |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="meshtastic/Meshtastic-Android"
pr="6618"
base_ref="$(gh pr view "$pr" --repo "$repo" --json baseRefName --jq .baseRefName)"
base="$(git merge-base HEAD "origin/$base_ref" 2>/dev/null || git merge-base HEAD "$base_ref")"
echo "=== PR metadata ==="
gh pr view "$pr" --repo "$repo" --json number,title,baseRefName,headRefName,commits,files \
--jq '{number,title,baseRefName,headRefName,commits:[.commits[]|{oid:.oid,messageHeadline:.messageHeadline}],files:[.files[].path]}'
echo "=== Changed files ==="
git diff --name-status "$base" HEAD
echo "=== Changed Kotlin outlines ==="
git diff --name-only "$base" HEAD -- '*.kt' | while IFS= read -r file; do
echo "--- $file"
ast-grep outline "$file" --items all --view compact || true
done
echo "=== Cache-update and import call sites ==="
rg -n -C 8 \
-e '\breplaceAllSettings\s*\(' \
-e '\btoChannelReplacementPlan\s*\(' \
-e '\bimportChannelSet\s*\(' \
-e '\bInstallProfileUseCase\b' \
. -g '*.kt'Length of output: 101633 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
echo "=== Focused production diffs ==="
git diff --no-ext-diff "$base" HEAD -- \
core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt \
core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt \
core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt \
core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt \
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt \
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
echo "=== Exact cache and transaction implementations ==="
for spec in \
"core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt:1:150" \
"core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt:35:180" \
"core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt:90:180" \
"core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt:25:70" \
"core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt:95:125"
do
file="${spec%%:*}"; rest="${spec#*:}"; start="${rest%%:*}"; end="${rest##*:}"
echo "--- $file:$start-$end"
sed -n "${start},${end}p" "$file" | nl -ba -v "$start"
done
echo "=== All updateChannelSet call sites ==="
rg -n -C 12 '\bupdateChannelSet\s*\(' . -g '*.kt'
echo "=== Cache-focused tests ==="
rg -n -C 10 \
-e 'effective.*[Ll]ora|[Ll]ora.*cache|cache.*[Ll]ora|updateChannelSet|cache.*unchanged|remote.*cache|write fails' \
core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt \
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt \
core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSourceTest.ktLength of output: 27568 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Current import implementations ==="
sed -n '45,185p' core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt
sed -n '90,165p' core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
sed -n '60,120p' core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt
echo "=== Changed-line locations ==="
git diff --unified=0 "$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)" HEAD -- \
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt \
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt \
core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt \
core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt
echo "=== Profile installation cache tests ==="
rg -n -C 12 \
-e 'refreshes local cache|remote profile|channel write fails|lora_config|currentChannelSet|settingsOperations' \
core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt
echo "=== QR import cache tests ==="
rg -n -C 12 \
-e 'channel write fails|lora_config|currentChannelSet|updateChannelSet|cache' \
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt
echo "=== Data-source atomic-update tests ==="
sed -n '70,135p' core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSourceTest.kt
echo "=== All direct channel-set cache writers ==="
rg -n -C 4 \
-e '\bsetLoraConfig\s*\(' \
-e '\breplaceAllSettings\s*\(' \
-e '\bupdateChannelSet\s*\(' \
core/data core/domain core/ui feature/settings -g '*.kt'Length of output: 76123
I found no new blocking runtime issues. The profile and QR import paths now update the cached I used the existing channel-replacement planner guidance during this review. One non-blocking documentation mismatch remains. In 🧠 Learnings usedIf you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit!
|
94d4a4d to
49e0960
Compare
|
Updated the stale cache-timing KDoc in |
49e0960 to
8df261e
Compare
|
Thanks again, @jamesarich — all requested changes are now addressed in
Focused JVM and Android-host tests, affected-module Spotless/Detekt, and |
8df261e to
7d729aa
Compare
jamesarich
left a comment
There was a problem hiding this comment.
Re-reviewed against my previous four items — all addressed, and addressed well:
- Rejection path surfaced —
installProfilenow catchesMalformedMeshtasticUrlExceptionand shows thechannel_invalidsnackbar, with a ViewModel test asserting the message. Making the exception anExpectedCondition(malformed-meshtastic-url) also keeps ordinary bad input out of Crashlytics/Datadog. ✔️ - Canonical path reused — the helpers now live once in
core:model(ChannelSetReplacement.kt) andimportChannelSetdelegates to the sametoChannelReplacementPlan, so profile installs getnormalizeReplacementSettingsfor free. The duplicate slot-count constant is gone. ✔️ - Local cache refresh — post-commit
updateChannelSeton local installs, atomic with the LoRa config,NonCancellable, skipped for remote destinations, and left untouched when a channel write fails mid-transaction. The test matrix covers all three. ✔️ - Minor items — LoRa is now written exactly once (
installConfigno longer writeslc.lora; the single write dedupes against the current config), and a blankchannel_urldegrades to "no channel set" instead of aborting. ✔️
The updateChannelSet(settingsList?, loraConfig?) null-preserves-field shape is a nice upgrade for importChannelSet too — the imported LoRa config no longer depends on cache-mirroring setConfig for its local visibility.
LGTM. Thanks for the thorough turnaround.
Summary
Device-profile export includes
channel_url, and the import dialog exposes it, but profile installation previously ignored that field and reported success without restoring channels.Impact
Installing an exported Device Profile now restores its normalized channel set and matching LoRa settings without duplicate RF writes or stale local channel state.
Validation
core:data,core:domain,core:repository,core:testing, andcore:uiSpotless, Detekt, andallTestspassed (358 tasks)core:model,core:domain,core:ui, andfeature:settingspassed (334 tasks)spotlessApply spotlessCheck detekt assembleDebug test allTests kmpSmokeCompilecompleted formatting and both debug APK assemblies, then hit the known unrelated Windows Android-host DataStore fixture rename failure in:core:database:allTests; the failure reproduced with:core:database:testAndroidHostTest --rerun-tasks --max-workers=1Summary by CodeRabbit
New Features
Bug Fixes