fix: nRF52 DFU flashing and frequency band detection#508
Merged
torlando-tech merged 9 commits intorelease/v0.8.xfrom Feb 20, 2026
Merged
fix: nRF52 DFU flashing and frequency band detection#508torlando-tech merged 9 commits intorelease/v0.8.xfrom
torlando-tech merged 9 commits intorelease/v0.8.xfrom
Conversation
After flashing, provisionDevice() now compares the device's reported firmware version against the expected version from the FirmwarePackage. If the device didn't actually reboot (e.g. DTR/RTS not wired), this catches the stale version and reports a clear error instead of false success. Also verifies firmware hash after writing it to EEPROM. Closes #489 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tation
Three protocol divergences caused repeated DFU failures that bricked
a Heltec T114. After reflashing with rnodeconf (which uses
adafruit-nrfutil), a byte-level comparison revealed the issues:
1. Init packet missing unconditional 2-byte suffix (int16_to_bytes(0x0000))
- Old: conditional odd-length padding (0 or 1 byte)
- New: always append [0x00, 0x00] matching reference
- Impact: wrong HCI payload length, header checksum, and CRC16
2. Missing flash page write waits after every 8 data packets
- The nRF52840 CPU is blocked during NVMC page writes (~103ms)
- HCI ACKs arrive before the write starts, so without the delay
we flood USB while the CPU can't service it
- Added FLASH_PAGE_WRITE_TIME_MS (103ms) delay every 8 packets
and after the last packet before DFU Stop
3. Post-DFU close-before-wait order reversed
- Reference: close port, then sleep (prevents USB interference
during bootloader CRC validation)
- Old: sleep first, then close
- New: disconnect before settle wait, finally block as safety net
Also simplified sendStartDfu() erase wait (2x → 1x, matching reference),
removed dead drainSerialBuffer() method, and added PACKETS_PER_PAGE and
FLASH_PAGE_WRITE_TIME_MS constants.
Critical USB fix: added startIoManager parameter to KotlinUSBBridge.connect().
The ioManager calls port.read() in a loop, and on timeout CdcAcmSerialDriver
calls testConnection() which sends USB GET_STATUS — a control transfer the
nRF52840 DFU bootloader's TinyUSB stack cannot handle, causing a USB bus
reset. NordicDFUFlasher now passes startIoManager=false to prevent the
ioManager from ever starting during DFU, using readBlockingDirect() and
writeBlockingDirect() (direct bulkTransfer) for all I/O.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The nRF52 DFU process involves two USB re-enumerations that Android treats as new devices (new device IDs, dropped permissions): 1. Pre-DFU: 1200-baud touch resets from app mode (PID 0x8071) to bootloader mode (PID 0x0071). Added findBootloaderDevice() to scan for the re-enumerated bootloader. Also detects when the device is already in bootloader mode and skips the touch. 2. Post-DFU: bootloader validates firmware and reboots back to app mode with yet another new device ID. Broadened provisionDevice() fallback scan to find any supported USB device (was ESP32-S3 only). Additional fixes: - Use startIoManager=false for 1200-baud touch connection (the ioManager's testConnection() crashes the nRF52840's USB stack) - Swap disconnect/disableRawMode order to prevent spurious ioManager restart on port close - Set bootloaderFlashModeActive=true during flash to suppress Android USB auto-navigation when device re-enumerates - Fix FrequencyBand.fromModelCode() to use full-byte lookup table matching rnodeconf reference (was nibble-based, wrong for many boards) - Fix RNodeDetector.getModelForBoardAndBand() to return per-board model codes matching rnodeconf (was returning same code for all boards) - Add FrequencyBand and model code unit tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…er re-enumeration After the 1200-baud touch, the device re-enumerates in bootloader mode with a new USB device ID. Android revokes the old permission and shows "Open with Columba?" — until the user taps it, connect() fails with "No permission". Previously this was a single attempt that failed immediately. Now retries up to 10 times (1s apart) to give the user time to grant permission. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Show "Verifying firmware..." instead of "Waiting for device reboot..." for nRF52, since the device already rebooted during DFU - Reduce post-DFU delay from 5s to 3s for nRF52 (only need USB re-enum time) - Keep bootloaderFlashModeActive through completion to prevent late USB re-enumeration from navigating away from the success screen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mission The USB device filter only had application-mode PIDs (0x8029, 0x8071, 0x80BA). When a 1200-baud touch resets an nRF52 into bootloader mode, the PID changes (e.g. 0x8071 → 0x0071). Without the bootloader PID in the filter, Android required manual "Open with Columba?" permission — causing DFU to fail when upgrading from running RNode firmware. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tatements Replace continue + inner loop continue with nested if-block and firstOrNull to reduce jump statements from 3 to 2 (break + return). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
Greptile SummaryFixes nRF52 DFU flashing by implementing proper Nordic DFU protocol with ACK-based flow control, USB re-enumeration handling, and raw mode communication to prevent USB controller crashes during flash operations. Key changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant App as FlasherViewModel
participant Flasher as NordicDFUFlasher
participant USB as KotlinUSBBridge
participant Device as nRF52 Device
App->>Flasher: flash(deviceId, firmware)
alt Device in application mode
Flasher->>USB: connect(deviceId, 1200 baud)
Flasher->>USB: setDtr(false)
Note over Flasher,Device: 1200-baud touch triggers reset
Flasher->>USB: disconnect()
Device->>Device: Re-enumerate as bootloader<br/>(PID 0x8071 → 0x0071)
Flasher->>Flasher: findBootloaderDevice()
Note over USB,Device: Android shows "Open with Columba?"<br/>or auto-grants via device_filter.xml
end
Flasher->>USB: connectWithRetry(bootloaderDeviceId, 115200)
Note over USB: Retry loop waits for<br/>USB permission grant
USB-->>Flasher: connected
Flasher->>USB: enableRawMode(drainPort=false)
Note over USB: Stop ioManager to prevent<br/>GET_STATUS during flash
Flasher->>USB: writeBlockingDirect(DFU Start)
USB->>Device: HCI packet via bulkTransfer
Device-->>USB: ACK
USB-->>Flasher: readBlockingDirect() → ACK
Note over Device: Erase flash pages<br/>(CPU blocked ~5-8s)
Flasher->>USB: writeBlockingDirect(DFU Init)
USB->>Device: Init packet
Device-->>USB: ACK
USB-->>Flasher: readBlockingDirect() → ACK
loop For each 512-byte packet
Flasher->>USB: writeBlockingDirect(DFU Data)
USB->>Device: Data packet
Device-->>USB: ACK
USB-->>Flasher: readBlockingDirect() → ACK
alt Every 8 packets (4096 bytes)
Note over Flasher: delay(103ms) for<br/>flash page write
end
end
Flasher->>USB: writeBlockingDirect(DFU Stop)
USB->>Device: Stop packet
Device-->>USB: ACK
USB-->>Flasher: readBlockingDirect() → ACK
Flasher->>USB: disconnect()
Flasher->>USB: disableRawMode()
Note over Device: Validate CRC, write<br/>bootloader settings (~200ms)
Device->>Device: Reset to application<br/>(PID 0x0071 → 0x8071)
App->>Flasher: provisionDevice()
Flasher->>Flasher: connectWithRetry() to re-enumerated device
Flasher->>Device: Verify firmware version + hash
Device-->>Flasher: version + hash match
Flasher-->>App: FlashState.Complete
Last reviewed commit: a053586 |
reticulum/src/main/java/com/lxmf/messenger/reticulum/flasher/NordicDFUFlasher.kt
Outdated
Show resolved
Hide resolved
reticulum/src/main/java/com/lxmf/messenger/reticulum/usb/KotlinUSBBridge.kt
Show resolved
Hide resolved
Remove enableDfuMode(), disableDfuMode(), stopIoManager(), and startIoManager() — all dead code from earlier DFU iterations. DFU now connects with startIoManager=false and uses readBlockingDirect() throughout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reticulum/src/main/java/com/lxmf/messenger/reticulum/flasher/NordicDFUFlasher.kt
Show resolved
Hide resolved
When the deadline elapsed between the while check and the read call, coerceIn(1, 200) masked the negative value into a 1ms timeout instead of exiting. Move remaining into the while condition and recompute at loop end. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
torlando-tech
added a commit
that referenced
this pull request
Feb 25, 2026
…atibility The startIoManager parameter added for nRF52 DFU (PR #508) removed the 2-parameter Java overload that Python/Chaquopy callers depend on. Without @jvmoverloads, Kotlin only generates connect(int, int, boolean) — so rnode_interface.py's connect(device_id, 115200) call fails with NoSuchMethodError, silently preventing USB RNode interfaces from starting. Fixes #545 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
torlando-tech
added a commit
that referenced
this pull request
Feb 25, 2026
…atibility The startIoManager parameter added for nRF52 DFU (PR #508) removed the 2-parameter Java overload that Python/Chaquopy callers depend on. Without @jvmoverloads, Kotlin only generates connect(int, int, boolean) — so rnode_interface.py's connect(device_id, 115200) call fails with NoSuchMethodError, silently preventing USB RNode interfaces from starting. Fixes #545 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This was referenced Feb 25, 2026
torlando-tech
added a commit
that referenced
this pull request
Feb 25, 2026
…atibility The startIoManager parameter added for nRF52 DFU (PR #508) removed the 2-parameter Java overload that Python/Chaquopy callers depend on. Without @jvmoverloads, Kotlin only generates connect(int, int, boolean) — so rnode_interface.py's connect(device_id, 115200) call fails with NoSuchMethodError, silently preventing USB RNode interfaces from starting. Fixes #545 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cherry-pick of #506 to
release/v0.8.x.usb_device_filter.xmlso Android auto-grants USB permission after the 1200-baud touch re-enumeration — this was the root cause of consistent flash failures when upgrading from running RNode firmwareFrequencyBand.fromModelCode()now uses full-byte lookup table matching rnodeconf, andgetModelForBoardAndBand()returns per-board model codesTest plan
🤖 Generated with Claude Code