Skip to content

fix(settings): restore channels from device profiles - #6618

Merged
jamesarich merged 1 commit into
meshtastic:mainfrom
simulationstation:fix/device-profile-channels
Aug 13, 2026
Merged

fix(settings): restore channels from device profiles#6618
jamesarich merged 1 commit into
meshtastic:mainfrom
simulationstation:fix/device-profile-channels

Conversation

@simulationstation

@simulationstation simulationstation commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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.

  • decode and validate a nonblank channel URL before opening the radio edit session; treat a blank URL as absent
  • use one shared channel-replacement planner for profile and QR imports, including blank-slot padding and semantic duplicate removal
  • authoritatively write all eight normalized channel slots after validating them
  • apply LoRa exactly once: a nonblank channel URL wins, the profile LoRa config is the fallback, and an unchanged active config is skipped
  • atomically refresh cached channel settings and applied LoRa only after successful local commits; remote and failed installs leave the cache untouched
  • surface malformed channel URLs through the existing invalid-channel snackbar as expected user input rather than error telemetry
  • cover empty, malformed, blank, duplicate, ordering, precedence, failure, remote, and local-cache behavior with regressions

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

  • affected core:data, core:domain, core:repository, core:testing, and core:ui Spotless, Detekt, and allTests passed (358 tasks)
  • prior affected-module validation for core:model, core:domain, core:ui, and feature:settings passed (334 tasks)
  • repository-wide spotlessApply spotlessCheck detekt assembleDebug test allTests kmpSmokeCompile completed 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=1

Summary by CodeRabbit

  • New Features

    • Profile installation now restores exported channel configurations alongside LoRa settings.
    • Channel roles, including primary, secondary, and disabled channels, are applied during installation.
    • Imported channels are normalized, including duplicate and unused channel slots.
    • Local channel settings are refreshed after successful local installation.
  • Bug Fixes

    • Invalid, empty, or oversized channel URLs are rejected before radio changes are made.
    • Redundant LoRa configuration updates are avoided.
    • Invalid channel imports now display a clear error notification.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0a09b65-0c35-4fb0-a44b-de016517e79a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

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

Changes

Profile installation

Layer / File(s) Summary
Normalize and validate channel replacements
core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt, core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.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
Shared utilities normalize placeholders and duplicates, validate channel limits, generate authoritative writes, and wrap decode failures as MalformedMeshtasticUrlException.
Apply profile channel and LoRa settings
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt, core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt, core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt
The use case validates channels before the transaction, writes channel and LoRa settings, updates local cache state, and records ordered fake-radio operations.
Use shared replacement planning for channel imports
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt, core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt
importChannelSet uses shared normalization and replacement planning for writes, cache updates, and placeholder detection.
Report malformed profile URLs
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt, feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
The view model passes current radio context to the use case and shows the invalid-channel snackbar for malformed URLs.

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
Loading

Possibly related PRs

Suggested reviewers: jamesarich, jeremiah-k

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Tests Prove The Path, Not The End State ⚠️ Warning The added blank-URL test seeds the cache and asserts the old cache plus profile LoRa behavior; the pre-PR use case already ignored channel_url and would satisfy those assertions. Make the blank-URL case prove an interaction-specific path, or remove it if no observable behavior distinguishes blank handling from the reverted implementation.
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Sibling Call Sites And Presence Semantics ✅ Passed The commit changes profile/channel handling only; all InstallProfileUseCase call sites use five arguments, channel_url consumers retain null checks, and no physical-metric field or NodeItem sibling...
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: restoring channel configurations during device profile installation.

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.

@github-actions github-actions Bot added the bugfix PR tag label Aug 11, 2026
@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from 7a18a02 to b53194a Compare August 11, 2026 21:59
@simulationstation
simulationstation marked this pull request as ready for review August 11, 2026 22:28

@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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between a22bf5e and b53194a.

📒 Files selected for processing (2)
  • 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

@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from b53194a to 7071a69 Compare August 11, 2026 22:42

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:

  • normalizeReplacementSettings is 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 with QueueStatus res=6 / routeErr=6. A profile's channel_url is 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 setChannel deliberately doesn't mirror to the cache (AdminControllerImpl.kt:238), which is why importChannelSet calls replaceAllSettings after commit. InstallProfileUseCase writes 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 remote destNum; installProfile can be either.) FakeRadioController records the writes, so no current test can catch this.

Minor

  • LoRa config is written twice per install: installConfig writes profile.config.lora, then the channel-set path writes channelSet.lora_config. importChannelSet guards this with takeIf { 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.

@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from 7071a69 to 69b1681 Compare August 12, 2026 01:24
@simulationstation

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the requested changes in 69b1681ac. QR and profile imports now share the canonical replacement planner in core:model, including padding and duplicate/blank normalization. Blank URLs are absent, malformed input is surfaced as the existing invalid-channel snackbar and classified as expected input, local installs refresh the cache only after success, and LoRa is written at most once with a nonblank URL taking precedence. Added regressions for empty/malformed/blank/duplicate/order/precedence/cache/failure cases; affected-module Spotless, Detekt, and allTests pass.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
🧩 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
done

Length 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.kt

Length of output: 95566


@simulationstation Full review complete. I found no new blocking issues.

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
Learnt from: simulationstation
URL: https://github.com/meshtastic/Meshtastic-Android/pull/6618

Timestamp: 2026-08-12T01:27:28.435Z
Learning: In Meshtastic-Android, full channel-set replacement imports must use `ChannelSet.toChannelReplacementPlan` from `core/model`. The planner normalizes blank and duplicate secondary `ChannelSettings`, enforces the eight firmware channel slots, and provides the authoritative channel writes. QR imports and device profile installation share this planner.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Full review finished.

@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 (1)
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt (1)

21-22: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert channel settings on recorded writes.

import_writes_all_eight_slots_with_replacement_roles checks indices, roles, and cache state, but not the settings sent by setChannel. 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 for importChannelSet.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6c06601 and 69b1681.

📒 Files selected for processing (11)
  • 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

@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from 69b1681 to 94d4a4d Compare August 12, 2026 01:41
@simulationstation

Copy link
Copy Markdown
Contributor Author

The cache-refresh follow-up is now addressed in 94d4a4da1: profile and QR imports share one atomic cached ChannelSet update, including the effective LoRa config, while omitted fields are preserved and remote/failed installs remain untouched. The focused 358-task affected-module gate passes. The inline thread has been replied to and resolved.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
🧩 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.kt

Length 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


@simulationstation Full review complete.

I found no new blocking runtime issues. The profile and QR import paths now update the cached ChannelSet only after a successful transaction. They use one atomic updateChannelSet call with normalized settings and the effective LoRa configuration. Remote and failed profile installs leave the local cache unchanged.

I used the existing channel-replacement planner guidance during this review.

One non-blocking documentation mismatch remains. In core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt, Lines 107-112 still describe LoRa cache updates as occurring during setConfig. The current code performs the combined cache update after the edit transaction. Please update that KDoc to match the implementation.


🧠 Learnings used
Learnt from: simulationstation
Repo: meshtastic/Meshtastic-Android PR: 0
File: :0-0
Timestamp: 2026-08-12T01:27:28.435Z
Learning: In Meshtastic-Android, full channel-set replacement imports must use `ChannelSet.toChannelReplacementPlan` from `core/model`. The planner normalizes blank and duplicate secondary `ChannelSettings`, enforces the eight firmware channel slots, and provides the authoritative channel writes. QR imports and device profile installation share this planner.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 35 minutes.

@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from 94d4a4d to 49e0960 Compare August 12, 2026 02:18
@simulationstation

Copy link
Copy Markdown
Contributor Author

Updated the stale cache-timing KDoc in 49e096094 to describe the post-commit atomic updateChannelSet behavior and LoRa preservation. :core:ui:spotlessCheck and :core:ui:detekt pass.

@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from 49e0960 to 8df261e Compare August 12, 2026 03:34

Copy link
Copy Markdown
Contributor Author

Thanks again, @jamesarich — all requested changes are now addressed in 8df261e8a:

  • malformed profile URLs are validated before the transaction, surfaced through the existing invalid-channel snackbar, and classified as expected user input;
  • profile and QR imports share the canonical core:model replacement planner, including blank/duplicate normalization;
  • successful local installs atomically refresh cached channels and the effective LoRa config, while remote and failed installs leave the cache unchanged;
  • LoRa is written at most once, nonblank URL LoRa wins, and blank channel_url is treated as absent;
  • the blank-URL regression now proves the exact post-commit interaction: no channel writes and one cache update with settingsList = null plus the profile LoRa value.

Focused JVM and Android-host tests, affected-module Spotless/Detekt, and kmpSmokeCompile all pass locally with four workers. Would you please take another look when convenient?

@simulationstation
simulationstation force-pushed the fix/device-profile-channels branch from 8df261e to 7d729aa Compare August 13, 2026 10:35

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed against my previous four items — all addressed, and addressed well:

  1. Rejection path surfacedinstallProfile now catches MalformedMeshtasticUrlException and shows the channel_invalid snackbar, with a ViewModel test asserting the message. Making the exception an ExpectedCondition (malformed-meshtastic-url) also keeps ordinary bad input out of Crashlytics/Datadog. ✔️
  2. Canonical path reused — the helpers now live once in core:model (ChannelSetReplacement.kt) and importChannelSet delegates to the same toChannelReplacementPlan, so profile installs get normalizeReplacementSettings for free. The duplicate slot-count constant is gone. ✔️
  3. Local cache refresh — post-commit updateChannelSet on 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. ✔️
  4. Minor items — LoRa is now written exactly once (installConfig no longer writes lc.lora; the single write dedupes against the current config), and a blank channel_url degrades 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.

@jamesarich
jamesarich added this pull request to the merge queue Aug 13, 2026
Merged via the queue into meshtastic:main with commit b4bedd9 Aug 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants