Skip to content

fix(firmware): repair nRF USB firmware update and post-update reconnect#6018

Merged
jamesarich merged 2 commits into
mainfrom
claude/optimistic-diffie-41f803
Jun 29, 2026
Merged

fix(firmware): repair nRF USB firmware update and post-update reconnect#6018
jamesarich merged 2 commits into
mainfrom
claude/optimistic-diffie-41f803

Conversation

@jamesarich

@jamesarich jamesarich commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Why

A community report ("can we do USB DFU for nRF nodes? I'm only seeing OTA") turned out not to be a missing feature — the USB (UF2) firmware-update path for nRF52 already exists, but it was broken end-to-end. Tapping Update via USB File Transfer always failed, and even when it flashed, the app couldn't reconnect afterward. This fixes the whole flow. Verified on a RAK4631 over USB.

🐛 Bug fixes

  • USB update never started. performUsbUpdate sets state to AwaitingFileSave — the deliberate pause where the UI launches the SAF file picker and saveDfuFile() resumes the flow. But FirmwareUpdateViewModel.startUpdate() / startUpdateFromFile() only handled Success/Error, so AwaitingFileSave fell into the else "defense-in-depth" branch and was overwritten with Error. Symptom in logcat: Firmware update returned without terminal state: AwaitingFileSave(...). Added an explicit AwaitingFileSave branch to both when blocks.

  • Post-update reconnect race. verifyUpdateResult forced a one-shot setDeviceAddress ~5s after reboot. A USB node re-enumerates through the bootloader over ~20s, so the forced reconnect fired into the enumeration gap, failed (Serial device not found), and landed the transport in Disconnected — which preempts the dedicated USB auto-recovery (observeUsbRecoveryTriggers, which only arms from DeviceSleep). Now we skip the forced reconnect for serial and let auto-recovery own it. BLE/TCP still force it (they have no hot-plug presence and the address is set to "n" during the update, so the explicit re-address is load-bearing there).

🛠️ Improvements

  • Stable USB serial identity. Devices were keyed by UsbDevice.deviceName (the /dev/bus/usb/BBB/DDD path), which Android reassigns on every re-enumeration — so after the post-update reboot the saved address no longer matched and auto-reconnect couldn't recognise the node. Now keyed by a stable vendorId-productId (UsbDevice.usbSerialStableKey()) in UsbRepository, and the saved SERIAL address is built from that key in AndroidGetDiscoveredDevicesUseCase (it previously baked in the path). Deliberately not the USB serial number — getSerialNumber() needs a permission grant that's missing right after re-enumeration, which would defeat the recovery. (Trade-off: two identical boards collide; documented in-code with the upgrade path.)

  • Recognise more nRF boards over USB. Registered 0x239A/0x4405 in ProbeTableProvider so Elecrow ThinkNode M3/M4/M6 and LilyGo T-Echo are detected as USB CDC serial devices at all (previously only RAK4631 0x8029 was). ⚠️ Unverified on hardware — I don't have an M3/M4/M6/T-Echo to test the connect path; the vid:pid is confirmed against the firmware board JSONs (build.hwids[0]). Happy to split this into its own PR if reviewers prefer to keep this one all-verified.

Testing performed

  • :feature:firmware:jvmTest — green (19 tests, incl. 2 new regression tests asserting startUpdate/startUpdateFromFile keep AwaitingFileSave instead of clobbering it to Error).
  • spotlessCheck + detekt — clean.
  • On-device (RAK4631, Pixel 6a, googleDebug): full USB update → file picker → flash → reboot → automatic reconnect with no manual replug. Logcat confirms DeviceSleep preserved → Post-update: leaving USB reconnect to USB auto-recoverySerial device connected in 23msConnected. The device's stable key (9114-32809) held across the path churn (/002 → … → /011).

Reviewer notes

  • The per-transport split in verifyUpdateResult is intentional and asymmetric: USB skips the forced reconnect (auto-recovery handles it), BLE/WiFi keep it (no presence signal; address is "n" post-update). See the in-code comment for the rationale.
  • Remaining known Android limitation (out of scope): USB permission is granted per UsbDevice instance and re-prompts on re-enumeration unless the user checked "always open this app" via the device_filter.xml attach dialog.

Summary by CodeRabbit

  • New Features

    • Improved USB device detection with expanded support for additional known VID/PID devices.
  • Bug Fixes

    • More reliable tracking of connected devices after re-enumeration (unplug/replug or reboot) by using a stable device identity for saved addressing.
    • USB firmware updates now preserve the “save file” step correctly and avoid unnecessary forced reconnects on Serial/USB while keeping reconnect behavior for other transports.
  • Tests

    • Added regression coverage for USB firmware update state preservation.

The USB (UF2) firmware-update path for nRF52 nodes was effectively broken
end-to-end. Three distinct issues, all fixed and verified on a RAK4631:

1. Update never started. performUsbUpdate sets state to AwaitingFileSave
   (the deliberate pause for the SAF file picker that saveDfuFile resumes),
   but FirmwareUpdateViewModel.startUpdate()/startUpdateFromFile() only
   handled Success/Error — AwaitingFileSave fell into the else
   "defense-in-depth" branch and was overwritten with Error. Tapping
   "Update via USB File Transfer" always failed. Add an explicit
   AwaitingFileSave branch to both when-blocks (+ regression tests).

2. Unstable device identity. USB serial devices were keyed by
   UsbDevice.deviceName (the /dev/bus/usb/BBB/DDD path), which Android
   reassigns on every re-enumeration. After the post-update reboot the node
   returns on a new path, so the saved address no longer resolved and
   auto-reconnect could not recognise it. Key by a stable
   vendorId-productId identity (UsbDevice.usbSerialStableKey()) in
   UsbRepository, and build the saved SERIAL address from that stable key
   in AndroidGetDiscoveredDevicesUseCase (it previously used the path).

3. Post-update reconnect race. verifyUpdateResult forced a one-shot
   setDeviceAddress ~5s after reboot. For USB the node re-enumerates
   through the bootloader over ~20s, so the forced reconnect fired into the
   enumeration gap, failed ("Serial device not found"), and landed in
   Disconnected — preempting the dedicated USB auto-recovery
   (observeUsbRecoveryTriggers), which only arms from DeviceSleep. Skip the
   forced reconnect for serial and let USB auto-recovery own it; BLE/TCP
   still need the explicit re-address (no hot-plug presence; address is set
   to "n" during the update).

Also register the 0x239A/0x4405 USB CDC id in ProbeTableProvider so Elecrow
ThinkNode M3/M4/M6 and LilyGo T-Echo are recognised as USB serial devices
at all (previously only RAK4631 0x8029 was). This piece is unverified on
hardware — see PR notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the bugfix PR tag label Jun 29, 2026
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d263f48-7576-4963-a2b0-6a4698229ef8

📥 Commits

Reviewing files that changed from the base of the PR and between 209d89d and e8c3186.

📒 Files selected for processing (1)
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt

📝 Walkthrough

Walkthrough

Adds a stable vendorId-productId USB identity, uses it for discovered-device addresses, registers an additional USB probe-table mapping, and updates firmware update handling so AwaitingFileSave is preserved and Serial/USB updates do not force reconnection.

Changes

USB device identity and firmware update flow

Layer / File(s) Summary
USB identity and probe table
core/network/.../UsbRepository.kt, core/network/.../ProbeTableProvider.kt
UsbDevice.usbSerialStableKey() returns a stable "$vendorId-$productId" key. serialDevices is rebuilt from probed drivers using that key. ProbeTableProvider adds VID/PID 9114/17413 with CdcAcmSerialDriver.
Device discovery uses stable address
feature/connections/.../AndroidGetDiscoveredDevicesUseCase.kt
USB fullAddress is derived from the stable key instead of deviceName/path.
Firmware AwaitingFileSave and reconnect handling
feature/firmware/.../FirmwareUpdateViewModel.kt, feature/firmware/.../FirmwareUpdateViewModelFileTest.kt
Both update entry points keep AwaitingFileSave as a no-op pause. Post-update reconnect is skipped for Serial/USB and kept for other transports. Tests assert the USB path remains in AwaitingFileSave.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped by USB and found a steady key,
VID and PID sang, “Stay with me.”
File save waits, then the update can gleam,
Serial stays calm, no forced reconnect dream.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main firmware USB update and reconnect fix without unnecessary noise.
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.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt`:
- Around line 62-66: The USB discovery map in UsbRepository currently replaces
the old path-based SERIAL key with only usbSerialStableKey(), which breaks saved
deviceName aliases and causes identical VID/PID boards to collide. Update the
device lookup/discovery flow to preserve backward compatibility by keeping an
alias or fallback for the old USB path key while also publishing the stable key,
and make sure duplicate devices are retained instead of silently overwriting
each other in the map. Apply the same fix in the related discovery block
referenced by the comment so existing installs still resolve previously saved
addresses.

In
`@feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt`:
- Around line 388-391: The post-update logging in FirmwareUpdateViewModel
currently prints fullAddr directly, which can expose device identifiers. Update
the Logger.i messages in the reconnect path to pass fullAddr through the
existing anonymize helper first, and ensure the anonymize import is present in
FirmwareUpdateViewModel. Keep the actual reconnect behavior via
radioController.setDeviceAddress unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 009e96ba-f7cc-4d70-958b-328b11ca4a8e

📥 Commits

Reviewing files that changed from the base of the PR and between f47c31d and 209d89d.

📒 Files selected for processing (5)
  • core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ProbeTableProvider.kt
  • core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt
  • feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
  • feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt

Addresses CodeRabbit review on #6018: fullAddr can contain a persisted BLE
MAC / TCP endpoint / serial id, so route it through the existing
core.model.util.anonymize helper before logging. Reconnect behaviour
(setDeviceAddress) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich added this pull request to the merge queue Jun 29, 2026
Merged via the queue into main with commit ce1a9fc Jun 29, 2026
18 checks passed
@jamesarich jamesarich deleted the claude/optimistic-diffie-41f803 branch June 29, 2026 20:33
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.

1 participant