Skip to content

fix(settings): stop dropping admin config responses (0% stall, missing remote channels) - #6391

Merged
jamesarich merged 6 commits into
mainfrom
claude/packet-authenticity-2-8-0-5b07c8
Jul 23, 2026
Merged

fix(settings): stop dropping admin config responses (0% stall, missing remote channels)#6391
jamesarich merged 6 commits into
mainfrom
claude/packet-authenticity-2-8-0-5b07c8

Conversation

@jamesarich

@jamesarich jamesarich commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Firmware 2.8 (meshtastic/firmware#10967) delivers a locally connected node's admin responses through a synchronous loopback, so the response now reaches the phone before the QueueStatus ack for the request that produced it. The app registered each request's id for response correlation only after the ack-awaiting send returned — the response sailed through meshPacketFlow while the id set was still empty, was silently dropped by the correlation guard, and every radio-config sub-screen stalled at a 0% loading overlay for 30s before a timeout dialog against 2.8 firmware. Root-caused with a dual-ended capture (app logcat + firmware serial): the device handled Get config: LoRa and enqueued its reply to the phone in milliseconds; the app dropped it.

The same audit closed two adjacent holes behind #6317 (remote channel list shows only the first channel; the community workaround was "press Cancel"):

Likely also resolves #5592 (settings screens hang at 0% indefinitely) — same symptom, though that report is on 2.7.x firmware while the root cause fixed here is the 2.8 loopback-ordering race; worth confirming the reporter's stall clears before closing.

🐛 Fixes

  • Register request ids before the send is issued. RadioConfigUseCase / AdminActionsUseCase request methods take an onRequestId callback invoked with the packet id ahead of the send; RadioConfigViewModel registers there instead of after the suspending call returns. Response ordering can no longer race registration, local or remote.
  • QueueStatus.res = 35 is not a failure. Firmware 2.8 returns its internal ERRNO_SHOULD_RELEASE ("delivered locally, packet consumed") for self-addressed packets; the handler treated any non-zero res as a failed send. Note: 35 numerically collides with Routing.Error.PKI_UNKNOWN_PUBKEYQueueStatus.res carries ErrorCode semantics, not Routing.Error (diagnosis trap). The handleQueueStatus early return is scoped to the plain res = 0 "accepted, now full" echo, so a res = 35 delivery still completes its response even when the TX queue is full (free = 0) — otherwise it would re-introduce the same 5s-timeout stall under queue pressure.
  • Channel editor now shows channels as they stream in ([Bug]: Not visible second channel for remote node #6317). The editable list was seeded once, at first composition — i.e. the instant channel 0 arrived — and never re-synced while a remote fetch streamed the rest in; the footer Cancel's replaceWith() was the accidental workaround. The editor adopts the authoritative list while the fetch is in flight (the loading overlay blocks edits during that window, so nothing can be clobbered).
  • Closed the request-chain gap. processPacketResponse removed the completed request id inline, and could observe a momentarily-empty id set in the gap between chained getChannel requests — tearing the flow down (clearPacketResponse) and stranding the rest of the chain with a partial list. The removal now runs behind any chain continuation launched by the same response (FIFO on the main dispatcher; registration is the continuation's first act).
  • NODEDB_RESET marks an expected local restart. Firmware reboots after a nodedb reset, but only FACTORY_RESET opened the restart window — a local nodedb reset surfaced the ensuing transport drop as a surprise disconnect instead of "restarting". NODEDB_RESET now calls expectRestart() for local resets, mirroring FACTORY_RESET.
  • A stale restart window can no longer mark an interrupted manual channel batch as "success". A manual channel batch shares the save shape (empty route + Loading) that the restart-success paths key on. A local MAY_RESTART save that opened the 90s window but never actually rebooted left that window open; a transient disconnect or request timeout during a subsequent batch would then flip the incomplete batch to Success, silently misreporting a partial channel write. Both completeRestartingSaveIfPending and the request-timeout backstop now gate on manualChannelBatchInFlight(), which tracks the batch from enqueue through its ack-wait (past finishManualChannelBatch) via the pending batch request ids — the enqueue flag alone left the ack-wait window exposed.

🧹 Follow-up (firmware, not this PR)

QueueStatus.res leaking internal ERRNO_* values to clients is worth an upstream firmware issue — mapping ERRNO_SHOULD_RELEASE → 0 would spare every client the same trap.

Testing Performed

  • New unit tests: getConfig invokes onRequestId with the packet id before issuing the send (ordering regression guard); handleQueueStatus treats ERRNO_SHOULD_RELEASE as success / other nonzero res as failure; handleQueueStatus completes ERRNO_SHOULD_RELEASE even when queue is full (the res = 35 + free = 0 queue-full path — covered by test, since on-device runs didn't exercise a full TX queue); NODEDB_RESET marks an expected local restart; manual channel batch is not completed as success by a stale restart window (this one caught an insufficient first fix that guarded only the enqueue phase, not the ack-wait — both restart-window edge cases are test-covered rather than device-exercised).
  • Full baseline spotlessApply spotlessCheck detekt assembleDebug test allTests green.
  • On-hardware before/after (Pixel 6a ↔ Heltec DUT + M5Stack Cardputer, both on 2.8.0 develop firmware): before — LoRa/Security/Channels all stall at 0% → 30s timeout; after — LoRa loads in ~3s fully enabled, Channels runs the complete sequential fetch (4 chained requests) and renders every channel with the editor kept in sync throughout.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved queue-status handling so a “should release” condition is treated as success.
    • Improved reliability completing radio/admin packet workflows and reducing request correlation timing issues.
  • New Features
    • Added expected-restart tracking to smooth local reboot/reconnect behavior.
    • Added reboot-behavior support for configuration saves (including save-and-restart messaging).
  • User Experience
    • Updated UI to clearly show “restarting” across connection/navigation and to prevent stale loading edits in channel config.
  • Tests
    • Added/updated coverage for restart tracking, request-id ordering, and queue-status success/failure cases.

@github-actions github-actions Bot added the bugfix PR tag label Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add request-ID callbacks before radio sends, track expected local node restarts, update connection and settings UI states, synchronize channel loading state, and treat queue status 35 as successful. Tests cover request ordering, restart lifecycle, queue completion, and updated view-model behavior.

Changes

Queue and request lifecycle

Layer / File(s) Summary
Queue status handling
core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt, core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
Queue status res == 35 completes requests successfully, including when the queue is full; other nonzero values remain failures.
Request-ID callback contracts
core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt, core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt, core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt
Administrative and radio configuration methods invoke optional request-ID callbacks before sending requests, with ordering tested for getConfig.
Settings request lifecycle
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 registers request IDs across configuration, admin, chained channel, and manual batch operations, and handles restart-related completion and timeout paths.

Restart-aware connection and settings UI

Layer / File(s) Summary
Expected restart tracking
core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRestartTracker.kt, core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt, core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt, core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/*
A timed restart window is tracked, cleared on reconnection or expiry, and exposed through connection and navigation state.
Reboot-aware settings feedback
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RebootBehavior.kt, feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/*, feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/*, core/resources/src/commonMain/composeResources/values/strings.xml
Settings screens pass reboot policies to save controls and dialogs, channel loading refreshes editable state, and localized restart labels are added.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RadioConfigViewModel
  participant NodeRestartTracker
  participant MeshConnectionManagerImpl
  participant ConnectionsViewModel
  participant SettingsDialog
  RadioConfigViewModel->>NodeRestartTracker: expectRestart()
  NodeRestartTracker-->>ConnectionsViewModel: restartExpected = true
  MeshConnectionManagerImpl->>NodeRestartTracker: onConnected()
  NodeRestartTracker-->>ConnectionsViewModel: restartExpected = false
  SettingsDialog-->>RadioConfigViewModel: display reboot-aware completion
Loading

Possibly related PRs

Suggested labels: refactor

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also adds restart-tracking and reboot-UI changes that are unrelated to #5592's settings-fetch hang fix. Split the restart/reboot behavior work into a separate PR or justify it in the issue; keep this PR focused on admin response handling.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address the 0% settings hang by registering request IDs before sends, handling QueueStatus 35, and fixing channel response flow.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix for stalled admin config responses and missing remote channels.

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.

@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

Caution

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

⚠️ Outside diff range comments (1)
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (1)

390-413: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unregister failed radio sends instead of waiting 30 seconds.

These methods call onRequestId(::registerRequestId) before the radioController.*/adminActionsUseCase.* send. safeLaunch catches throwables, logs unknown_error, and returns normally, so a send failure after ID registration leaves the request in Loading with a pending 30s timeout. Wrap the send area with cleanup for the just-registered request IDs/request timeouts, or move registration/timeout cancellation out of the send path unless registration is guaranteed to complete successfully.

🤖 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
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`
around lines 390 - 413, Update setHamMode and setOwner so a failed send cannot
leave the newly registered request in Loading until the 30-second timeout. Track
the request ID registered by onRequestId and clean up its request state/timeout
when radioConfigUseCase.setHamMode or setOwner throws within safeLaunch, or move
registration until the send is guaranteed to succeed while preserving successful
request tracking.
🤖 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/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt`:
- Around line 175-177: Update the success/full early-return condition in the
queue status handling of PacketHandlerImpl so it returns only when res == 0 and
free == 0; ERRNO_SHOULD_RELEASE must continue to queueResponse even when the
queue is full. Add a regression test covering res == ERRNO_SHOULD_RELEASE with
free == 0 and verifying queueResponse completes.

---

Outside diff comments:
In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Around line 390-413: Update setHamMode and setOwner so a failed send cannot
leave the newly registered request in Loading until the 30-second timeout. Track
the request ID registered by onRequestId and clean up its request state/timeout
when radioConfigUseCase.setHamMode or setOwner throws within safeLaunch, or move
registration until the send is guaranteed to succeed while preserving successful
request tracking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d5d0d20-ad28-4adb-8609-68853bc0b2ec

📥 Commits

Reviewing files that changed from the base of the PR and between 959d51c and ee2564f.

📒 Files selected for processing (8)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
  • core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt
  • core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt
  • core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt

Comment on lines 175 to 177
val (success, isFull, requestId) =
with(queueStatus) { Triple(res == 0 || res == ERRNO_SHOULD_RELEASE, free == 0, mesh_packet_id) }
if (success && isFull) return

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not suppress ERRNO_SHOULD_RELEASE when the queue is full.

Because status 35 now makes success true, res == 35 && free == 0 returns before completing queueResponse. A locally delivered packet can therefore time out whenever the TX queue has no free slots. Restrict the early return to res == 0 && free == 0, and add a regression test for this combination.

🤖 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/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt`
around lines 175 - 177, Update the success/full early-return condition in the
queue status handling of PacketHandlerImpl so it returns only when res == 0 and
free == 0; ERRNO_SHOULD_RELEASE must continue to queueResponse even when the
queue is full. Add a regression test covering res == ERRNO_SHOULD_RELEASE with
free == 0 and verifying queueResponse completes.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Remote admin verification (the #6317 scenario, on-hardware):

Setup: Pixel 6a ↔ M5Stack Cardputer (local, BLE, fw 2.8.0.d9150e8) remote-administering a Heltec V3 (!483ba531, fw 2.8.0.8abae90) over LoRa via PKC admin.

Result with this branch: opening the remote Channels screen runs the full sequential fetch — getChannel(0)getLoraConfiggetChannel(1)getChannel(2)getChannel(3)(DISABLED, terminates) — each request chained ~4s after the previous reply (LoRa RTT). All three channels (primary + two secondaries) stream into the editor as they arrive and the loading overlay clears cleanly at the end. No premature flow teardown, no 30s timeout, no partial list, and no need for the "press Cancel" workaround (which this audit traced to the footer Cancel's replaceWith() acting as an accidental refresh of the stale editor snapshot).

🤖 Generated with Claude Code

jamesarich and others added 4 commits July 23, 2026 13:48
Radio-config sub-screens correlate admin responses to requests via a
request-id set, but the id was registered only after the suspending send
returned — and the send suspends until the radio acks the packet via
QueueStatus. Firmware 2.8 (meshtastic/firmware#10967) routes self-addressed
packets through a synchronous local loopback, so the admin response now
reaches the phone BEFORE that ack. The response flowed through
meshPacketFlow while the request-id set was still empty, was silently
dropped by the correlation guard, and every local config screen sat at a 0%
loading overlay until the 30s timeout.

Every request method on RadioConfigUseCase / AdminActionsUseCase now takes
an onRequestId callback invoked with the packet id before the send is
issued, and RadioConfigViewModel registers there instead of after the call.
The manual-channel batch helper threads the same callback through
writeChannel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ccess

Firmware 2.8 returns its internal ErrorCode ERRNO_SHOULD_RELEASE (35, "no
error, but the packet should still be released", MeshTypes.h) in
QueueStatus.res for self-addressed packets delivered through the
synchronous local loopback instead of the TX queue. The handler treated
any non-zero res as a failed send, marking every local admin/telemetry
request "success false".

Note 35 numerically collides with Routing.Error.PKI_UNKNOWN_PUBKEY —
QueueStatus.res carries ErrorCode semantics, not Routing.Error, so this is
a benign local delivery, not a PKI failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…equest-chain gap

Fixes #6317.

The channel editor copied the authoritative channel list into its editable
snapshot exactly once, at first composition — which happens the moment
channel 0 arrives. A remote channel fetch streams the remaining channels in
one response at a time after that seed, so they never rendered; the footer
Cancel button's replaceWith() was the accidental community workaround.
The editor now adopts the authoritative list while the fetch is in flight
(the loading overlay blocks user edits during that window, so no edits can
be clobbered).

Also defer the request-id removal in processPacketResponse behind any
chain continuation launched by the same response (launches run FIFO on the
main dispatcher, and registration is the continuation's first act). The
inline removal could observe a momentarily-empty request set in the gap
between chained getChannel requests, tear down the whole flow via
clearPacketResponse, and strand the rest of the chain — leaving a partial
channel list and a stranded loading overlay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firmware applies most config sections with a node reboot a few seconds after
the save is acked (module saves even disable Bluetooth immediately). Today
the app cries wolf — every save shows the same "may disconnect and reboot"
line, including saves that never reboot — and the resulting BLE drop renders
as an alarming red "disconnected" that looks identical to a real failure.
Worse, the save's own response waits for a routing ACK the reboot eats, so
it sits at 0% for 30s then throws a spurious "Timeout" error.

Three changes:

**Attribute the disconnect.** A NodeRestartTracker opens a ~90s "expected
restart" window at send time whenever a LOCAL save/action reboots the
connected node (reboot-applying config/module sections, explicit reboot,
factory reset, ham-mode). Remote destinations don't open it — a remote reboot
doesn't drop our transport. While open, the transport drop presents as
ConnectionStatus.RESTARTING ("Restarting…" on the Connection card, orange
connecting-treatment nav icon instead of red) and the foreground-service
notification keeps the connecting presentation. The window closes when the
post-reboot handshake completes (ConnectionState.Connected) or expires.

**Complete the save on reboot.** A reboot-applying save can't survive the
reboot it triggers. The transport-drop during the restart window IS the
confirmation the save was persisted (firmware only reboots after saveChanges
writes to disk), so the pending save resolves to a "node is restarting"
success the moment the node drops, rather than hanging to a 30s timeout error.
The request timeout carries a matching backstop.

**Honest consent.** A coarse RebootBehavior map (ALWAYS / MAY_RESTART / NEVER)
mirrors the firmware's per-section reboot decision. Always-reboot sections
(Position, Network, Bluetooth, Security + every module except status message)
show a "Save & restart" footer and a "the node is restarting" success notice;
field-dependent sections keep the softer "may reboot" copy; sections that
never reboot (Channels, Status Message) drop the warning entirely. The map is
deliberately coarse — firmware's decision is field-level and drifts between
releases, so the app never claims precision it can't keep.

Verified on hardware (Pixel 6a ↔ Heltec DUT, fw 2.8.0): saving Position shows
"Save & restart", resolves to the restarting-success (no more 0%/timeout),
presents "Restarting…" with an orange (not red) nav icon throughout the
disconnect, and clears to Connected on reconnect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich
jamesarich force-pushed the claude/packet-authenticity-2-8-0-5b07c8 branch from 4b29b97 to 33b961c Compare July 23, 2026 18:48
… full

Widening handleQueueStatus success to include ERRNO_SHOULD_RELEASE (35) also
widened the `success && free == 0` early return, so a self-addressed
local-loopback delivery (res=35) that coincided with a full TX queue (free=0)
returned before completing queueResponse — hanging until the 5s TIMEOUT, the
exact stall this branch set out to fix.

Scope the early return to the plain res=0 "accepted, now full" echo. res=35
now always completes its response. Adds a regression test for res=35 + free=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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

🧹 Nitpick comments (5)
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt (2)

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

Same real-dispatcher NodeRestartTracker construction as the other ViewModel test files.

See consolidated comment (anchored on RadioConfigViewModelTest.kt) for the shared fix.

🤖 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/viewmodel/ConnectionsViewModelTest.kt`
at line 66, Update the nodeRestartTracker initialization in
ConnectionsViewModelTest to use the same test dispatcher and lifecycle setup as
the consolidated fix in the other ViewModel tests, rather than constructing
NodeRestartTracker with a standalone CoroutineScope(SupervisorJob()).

156-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test covers the new ConnectionStatus.RESTARTING mapping.

connectionStatus now maps Connecting/Disconnected to RESTARTING when nodeRestartTracker.restartExpected is true, but no test in this file drives nodeRestartTracker.expectRestart() to exercise that branch — existing tests only cover the pre-existing CONNECTING/RECONNECTING/NOT_CONNECTED paths. Worth adding a test analogous to Connecting state maps to CONNECTING regardless of progress text that calls nodeRestartTracker.expectRestart() first and asserts RESTARTING for both Connecting and Disconnected.

🤖 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/viewmodel/ConnectionsViewModelTest.kt`
around lines 156 - 182, The connection status tests do not cover the RESTARTING
mapping when a restart is expected. Add a test in ConnectionsViewModelTest
analogous to the existing Connecting-state test, call
nodeRestartTracker.expectRestart() before changing states, and assert RESTARTING
for both Connecting and Disconnected states.
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (1)

371-382: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

LGTM overall on the restart-window plumbing; minor style inconsistency.

FACTORY_RESET calls nodeRestartTracker.expectRestart() directly instead of going through the expectRestartIfLocal helper used everywhere else (setHamMode, setConfig, setModuleConfig, REBOOT). Functionally equivalent since it's already gated by isLocal, but routing through the shared helper would keep the "who opens the window" logic in one place.

Also applies to: 601-605, 618-623

🤖 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
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`
around lines 371 - 382, Route the FACTORY_RESET handling, including the related
paths around setHamMode/setConfig/setModuleConfig and REBOOT, through
expectRestartIfLocal instead of calling nodeRestartTracker.expectRestart()
directly. Preserve the existing local-node and reboot-behavior gating while
centralizing restart-window triggering in the shared helper.
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt (1)

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

Same real-dispatcher NodeRestartTracker construction as the other ViewModel test files.

See consolidated comment (anchored on RadioConfigViewModelTest.kt) for the shared fix.

🤖 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
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt`
at line 92, Update ProfileRoundTripTest’s nodeRestartTracker initialization to
use the same test-safe dispatcher or coroutine-scope construction established by
the consolidated fix in RadioConfigViewModelTest.kt, rather than creating
NodeRestartTracker with a real-dispatcher CoroutineScope. Keep the tracker
behavior and test lifecycle consistent with the other ViewModel tests.
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt (1)

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

NodeRestartTracker backed by a real dispatcher instead of the test's virtual-time scheduler.

Unlike NodeRestartTrackerTest.kt and MeshConnectionManagerImplTest.kt (which use backgroundScope), this constructs NodeRestartTracker(CoroutineScope(SupervisorJob())) — a scope with no TestDispatcher, so its internal delay(window) expiry job runs on Dispatchers.Default in real time, unaffected by advanceTimeBy/runCurrent, and is never cancelled at teardown. See consolidated comment for the shared fix across this file, ProfileRoundTripTest.kt, and ConnectionsViewModelTest.kt.

🤖 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
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`
at line 126, Update the NodeRestartTracker test fixture to construct it with the
test-managed backgroundScope instead of CoroutineScope(SupervisorJob()). Ensure
the tracker uses the test scheduler for virtual-time control and is cancelled
automatically during teardown, matching the setup in NodeRestartTrackerTest and
MeshConnectionManagerImplTest.
🤖 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
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Around line 865-879: Prevent stale restart expectations from completing manual
channel batches as successful saves. Update completeRestartingSaveIfPending and
the registerRequestId timeout backstop to require the pending loading state is
not a beginManualChannelBatch operation, while preserving the existing
restartExpected and empty-route checks.
- Around line 625-629: Update the NODEDB_RESET branch in the safeLaunch block to
call nodeRestartTracker.expectRestart() when isLocal is true, immediately before
adminActionsUseCase.nodedbReset(...). Keep remote resets unchanged and match the
existing FACTORY_RESET behavior.

---

Nitpick comments:
In
`@core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt`:
- Line 66: Update the nodeRestartTracker initialization in
ConnectionsViewModelTest to use the same test dispatcher and lifecycle setup as
the consolidated fix in the other ViewModel tests, rather than constructing
NodeRestartTracker with a standalone CoroutineScope(SupervisorJob()).
- Around line 156-182: The connection status tests do not cover the RESTARTING
mapping when a restart is expected. Add a test in ConnectionsViewModelTest
analogous to the existing Connecting-state test, call
nodeRestartTracker.expectRestart() before changing states, and assert RESTARTING
for both Connecting and Disconnected states.

In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Around line 371-382: Route the FACTORY_RESET handling, including the related
paths around setHamMode/setConfig/setModuleConfig and REBOOT, through
expectRestartIfLocal instead of calling nodeRestartTracker.expectRestart()
directly. Preserve the existing local-node and reboot-behavior gating while
centralizing restart-window triggering in the shared helper.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt`:
- Line 92: Update ProfileRoundTripTest’s nodeRestartTracker initialization to
use the same test-safe dispatcher or coroutine-scope construction established by
the consolidated fix in RadioConfigViewModelTest.kt, rather than creating
NodeRestartTracker with a real-dispatcher CoroutineScope. Keep the tracker
behavior and test lifecycle consistent with the other ViewModel tests.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`:
- Line 126: Update the NodeRestartTracker test fixture to construct it with the
test-managed backgroundScope instead of CoroutineScope(SupervisorJob()). Ensure
the tracker uses the test scheduler for virtual-time control and is cancelled
automatically during teardown, matching the setup in NodeRestartTrackerTest and
MeshConnectionManagerImplTest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fcb7e481-dd45-4eb6-9f11-75ad31578a8b

📥 Commits

Reviewing files that changed from the base of the PR and between ee2564f and 33b961c.

📒 Files selected for processing (43)
  • .skills/compose-ui/strings-index.txt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
  • core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt
  • core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt
  • core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRestartTracker.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt
  • core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/NodeRestartTrackerTest.kt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
  • core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RebootBehavior.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
🚧 Files skipped from review as they are similar to previous changes (5)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
  • core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt
  • core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt

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

🧹 Nitpick comments (1)
core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt (1)

137-151: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the preserved res == 0 && free == 0 branch.

These tests prove that res = 35 completes successfully when the queue is full, but do not protect the intentional early return for ordinary successful responses with free = 0. Add a regression test that confirms such a response does not complete the awaited result.

🤖 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/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt`
around lines 137 - 151, Add a test alongside handleQueueStatus completes
ERRNO_SHOULD_RELEASE even when queue is full that starts sendToRadioAndAwait,
processes QueueStatus with the matching mesh_packet_id, res = 0, and free = 0,
then verifies the awaited result remains incomplete without hanging the test.
Preserve the existing res = 35 coverage and assert the plain successful
full-queue response follows the intentional early-return path.
🤖 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.

Nitpick comments:
In
`@core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt`:
- Around line 137-151: Add a test alongside handleQueueStatus completes
ERRNO_SHOULD_RELEASE even when queue is full that starts sendToRadioAndAwait,
processes QueueStatus with the matching mesh_packet_id, res = 0, and free = 0,
then verifies the awaited result remains incomplete without hanging the test.
Preserve the existing res = 35 coverage and assert the plain successful
full-queue response follows the intentional early-return path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a564cbc6-ed5c-41ea-a7ea-d9bccd552f1c

📥 Commits

Reviewing files that changed from the base of the PR and between 33b961c and 74b6aac.

📒 Files selected for processing (2)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt

…anual channel batches

Two CodeRabbit findings on the reboot-visibility change:

- NODEDB_RESET now opens the expected-restart window for a local reset, mirroring
  FACTORY_RESET. Firmware reboots after a nodedb reset, so without this the ensuing
  transport drop surfaced as a surprise disconnect instead of "restarting".

- A manual channel batch shares the save shape (empty route + Loading) that the
  restart-success paths key on, so a stale restart window (a local MAY_RESTART save
  that never actually rebooted) could flip an incomplete batch to Success on a
  transient disconnect or request timeout — silently misreporting a partial channel
  write. Gate both completeRestartingSaveIfPending and the request-timeout backstop
  on manualChannelBatchInFlight(), which tracks the batch from enqueue through its
  ack-wait (past finishManualChannelBatch) via the pending batch request ids — the
  enqueue flag alone leaves the ack-wait window exposed.

Adds regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich
jamesarich added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit c07c914 Jul 23, 2026
17 checks passed
@jamesarich
jamesarich deleted the claude/packet-authenticity-2-8-0-5b07c8 branch July 23, 2026 20:14
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.

[Bug]: Settings configuration hangs at 0% on Google Pixel 10 Pro (Bluetooth 6.0 / Android 16)

1 participant