Skip to content

fix: add post-flash firmware verification#497

Merged
torlando-tech merged 1 commit intomainfrom
fix/rnode-flasher-post-flash-verification
Feb 20, 2026
Merged

fix: add post-flash firmware verification#497
torlando-tech merged 1 commit intomainfrom
fix/rnode-flasher-post-flash-verification

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Adds post-flash firmware version comparison in provisionDevice() — compares device-reported version against expected version from FirmwarePackage
  • Adds firmware hash read-back verification after writing to EEPROM
  • If the device didn't actually reboot after flashing (e.g. DTR/RTS lines not wired), the user now gets a clear error instead of false success

Closes #489

Test plan

  • Build: ./gradlew :reticulum:compileDebugKotlin passes
  • Existing FlashState tests pass: ./gradlew :reticulum:testDebugUnitTest --tests "*.FlashState*"
  • Manual: Flash an RNode that reboots correctly — version matches, success as before
  • Manual: Flash an RNode where hard reset doesn't work — error shown instead of false success

🤖 Generated with Claude Code

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

greptile-apps bot commented Feb 18, 2026

Greptile Summary

This PR adds post-flash firmware verification to RNodeFlasher.provisionDevice(), addressing #489 where a device that fails to reboot after flashing (e.g., DTR/RTS lines not wired) would silently report success.

  • Firmware version comparison: After reconnecting to the device post-flash, the reported firmwareVersion is compared against the expected version from FirmwarePackage.version. If they don't match, a clear error is shown instead of false success.
  • Firmware hash read-back verification: After writing the firmware hash to EEPROM via setFirmwareHash(), the actual boot-computed hash is read back via getFirmwareHash() and compared against the pre-calculated binary hash. A mismatch indicates the device is still running old firmware.
  • Both checks are applied in two code paths within provisionDevice(): the "already provisioned" branch and the "fresh provisioning + reset" branch.
  • The flashFirmwareAutoDetect path and onDeviceManuallyReset path don't receive expectedFirmwareVersion since they don't have access to a FirmwarePackage, so the version check is skipped there (hash check may still apply if firmwareHash is provided).
  • The verification logic is duplicated across three locations within provisionDevice(); extracting it into helper methods would improve maintainability.

Confidence Score: 4/5

  • This PR is safe to merge — it adds defensive verification checks without altering the happy path behavior.
  • The core verification logic is correct: firmware version string comparison and boot-computed hash vs binary hash comparison will reliably detect the target failure mode (device not rebooting after flash). The new parameter defaults to null, preserving backward compatibility. The main concern is the missing expectedFirmwareVersion in the onDeviceManuallyReset path, which is a gap in coverage rather than a correctness issue.
  • Pay attention to onDeviceManuallyReset (line 900) — it's the one code path that calls provisionDevice without the new expectedFirmwareVersion parameter, meaning manual-reset flows skip version verification.

Important Files Changed

Filename Overview
reticulum/src/main/java/com/lxmf/messenger/reticulum/flasher/RNodeFlasher.kt Adds firmware version and hash verification to provisionDevice(). Logic is sound: version string comparison and boot-hash vs binary-hash comparison correctly detect failed reboots. Some code duplication across three verification points; onDeviceManuallyReset path doesn't forward expectedFirmwareVersion.

Sequence Diagram

sequenceDiagram
    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
Loading

Last reviewed commit: 8d642dd

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +759 to +780
// 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}")
}
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.

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 18, 2026

Additional Comments (1)

reticulum/src/main/java/com/lxmf/messenger/reticulum/flasher/RNodeFlasher.kt
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?

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

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@torlando-tech torlando-tech merged commit db7f0b4 into main Feb 20, 2026
23 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RNode flasher: RNode do not reboot when Columba thinks it would, no firmware version check

1 participant