fix: add post-flash firmware verification#497
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>
Greptile SummaryThis PR adds post-flash firmware verification to
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as FlasherViewModel
participant F as RNodeFlasher
participant D as RNodeDetector
participant Dev as RNode Device
UI->>F: flashFirmware(deviceId, firmwarePackage)
F->>F: calculateFirmwareBinaryHash()
F->>Dev: Flash firmware (ESP32/NRF52)
F->>F: Wait for reboot
F->>F: provisionDevice(deviceId, board, band, firmwareHash, expectedVersion)
alt Device already provisioned
F->>D: getDeviceInfo()
D->>Dev: Query firmware version
Dev-->>D: firmwareVersion
D-->>F: RNodeDeviceInfo
Note over F: NEW: Version verification
F->>F: Compare deviceInfo.firmwareVersion vs expectedVersion
alt Version mismatch
F-->>UI: FlashState.Error("version mismatch")
end
F->>D: setFirmwareHash(firmwareHash)
D->>Dev: Write target hash to EEPROM
Note over F: NEW: Hash verification
F->>D: getFirmwareHash()
D->>Dev: Read actual boot-computed hash
Dev-->>D: actualHash
D-->>F: actualHash
F->>F: Compare actualHash vs firmwareHash
alt Hash mismatch
F-->>UI: FlashState.Error("hash mismatch")
end
F-->>UI: FlashState.Complete
else Device needs provisioning
F->>D: provisionAndSetFirmwareHash()
D->>Dev: Write EEPROM + hash
F->>D: resetDevice()
F->>F: Wait for reboot
F->>D: getDeviceInfo()
D-->>F: verifiedInfo
Note over F: NEW: Post-provision version verification
F->>F: Compare verifiedInfo.firmwareVersion vs expectedVersion
alt Version mismatch
F-->>UI: FlashState.Error("version mismatch")
end
F-->>UI: FlashState.Complete
end
Last reviewed commit: 8d642dd |
| // Verify firmware version if expected version is known | ||
| if (expectedFirmwareVersion != null && deviceInfo.firmwareVersion != null) { | ||
| if (deviceInfo.firmwareVersion != expectedFirmwareVersion) { | ||
| Log.e( | ||
| TAG, | ||
| "Firmware version mismatch: expected=$expectedFirmwareVersion, " + | ||
| "actual=${deviceInfo.firmwareVersion}", | ||
| ) | ||
| _flashState.value = | ||
| FlashState.Error( | ||
| "Flash verification failed: device reports firmware " + | ||
| "${deviceInfo.firmwareVersion} but expected " + | ||
| "$expectedFirmwareVersion. The device may not have rebooted " + | ||
| "after flashing. Try manually resetting the device and " + | ||
| "re-flashing.", | ||
| recoverable = true, | ||
| ) | ||
| usbBridge.disconnect() | ||
| return@withContext false | ||
| } | ||
| Log.i(TAG, "Firmware version verified: ${deviceInfo.firmwareVersion}") | ||
| } |
There was a problem hiding this comment.
Duplicated verification blocks
The firmware version verification logic (check + error state + disconnect + return false) is repeated identically in three places: here (line 760), line 849, and with slight variation in between. Consider extracting this into a private helper function like verifyFirmwareVersion(expected, actual, usbBridge): Boolean to reduce duplication and make future error message updates easier to maintain.
Similarly, the firmware hash verification block (lines 790-808) could be extracted. This isn't urgent, but would improve maintainability since all three version checks use the same error message and recovery logic.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: reticulum/src/main/java/com/lxmf/messenger/reticulum/flasher/RNodeFlasher.kt
Line: 759-780
Comment:
**Duplicated verification blocks**
The firmware version verification logic (check + error state + disconnect + return false) is repeated identically in three places: here (line 760), line 849, and with slight variation in between. Consider extracting this into a private helper function like `verifyFirmwareVersion(expected, actual, usbBridge): Boolean` to reduce duplication and make future error message updates easier to maintain.
Similarly, the firmware hash verification block (lines 790-808) could be extracted. This isn't urgent, but would improve maintainability since all three version checks use the same error message and recovery logic.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
Prompt To Fix With AIThis is a comment left during a code review.
Path: reticulum/src/main/java/com/lxmf/messenger/reticulum/flasher/RNodeFlasher.kt
Line: 900-907
Comment:
**Missing `expectedFirmwareVersion` in manual reset path**
`onDeviceManuallyReset` calls `provisionDevice` without passing `expectedFirmwareVersion`, so the post-flash firmware version check is skipped when users manually reset the device. Since this is one of the scenarios most likely to have a stale firmware version (manual reset might not be done correctly), it may be worth propagating the expected version through this path as well — e.g., storing it in the `NeedsManualReset` state alongside `firmwareHash`. Was the omission of `expectedFirmwareVersion` from `onDeviceManuallyReset` intentional? The `NeedsManualReset` state already carries `firmwareHash` — should it also carry the expected firmware version so the manual-reset provisioning path gets the same verification?
How can I resolve this? If you propose a fix, please make it concise. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Summary
provisionDevice()— compares device-reported version against expected version fromFirmwarePackageCloses #489
Test plan
./gradlew :reticulum:compileDebugKotlinpasses./gradlew :reticulum:testDebugUnitTest --tests "*.FlashState*"🤖 Generated with Claude Code