Skip to content

feat(firmware): Confirm local firmware files before flashing#6083

Draft
jeremiah-k wants to merge 3 commits into
meshtastic:mainfrom
jeremiah-k:enhancement/local-firmware-file-confirmation
Draft

feat(firmware): Confirm local firmware files before flashing#6083
jeremiah-k wants to merge 3 commits into
meshtastic:mainfrom
jeremiah-k:enhancement/local-firmware-file-confirmation

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR makes local firmware file selection safer.

Previously, selecting a file from the Android picker immediately started the update. On devices where long filenames are truncated, it was easy to select the wrong release bundle or firmware artifact without seeing the full name first.

This change resolves and validates the selected file, shows the full filename, and requires explicit confirmation before flashing begins.

Key Changes

  • Resolved the selected file display name.
  • Added pending local firmware selection state.
  • Required explicit confirmation before starting a local-file update.
  • Showed the full filename, device name, and platformio target before flashing.
  • Made the filename selectable for easier inspection.
  • Validated local firmware filenames against the current device and update method.
  • Revalidated pending selections immediately before starting the update.
  • Rejected stale selections when target, update method, or address changed before confirmation.
  • Supported selecting official release bundles and extracting the matching update payload.
  • Revalidated extracted payloads before confirmation.
  • Preserved exclusions for non-app artifacts.
  • Cleaned up temporary extracted files on dismiss, error, state change, and ViewModel clear.
  • Preserved USB file-save state during local and downloaded update flows.
  • Added focused validation and ViewModel coverage.

Testing

  • Added coverage for local firmware filename validation.
  • Added coverage for pending-file confirmation.
  • Added coverage for stale target, update method, and address rejection.
  • Added coverage for release bundle extraction.
  • Added coverage for temporary artifact cleanup.
  • Added coverage for preserving USB file-save state.
  • Verified manually on device through the combined firmware-update flow.
  • Verified git diff --check passes.

Migration Notes

  • No user data migration is required.
  • Users can continue selecting official release bundles.
  • This only adds validation and confirmation before local firmware flashing begins.

Summary by CodeRabbit

  • New Features

    • Added a two-step local firmware update flow with a confirmation dialog before starting the update.
    • Improved local firmware filename display by deriving human-readable names from the selected URI.
  • Bug Fixes

    • Strengthened local firmware validation (required payload extensions by device/update method, target matching, and stale selection/context detection).
    • Enhanced recovery/update messaging for missing targets, unsupported methods, and invalid or unavailable filenames.
    • Improved pending-selection cleanup and cancellation behavior.
  • Tests

    • Expanded coverage for local firmware validation and the new pending/confirm flow.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 29e9ffd8-c278-4342-8c35-ccb4a505262a

📥 Commits

Reviewing files that changed from the base of the PR and between efa29bd and 0fdba9a.

📒 Files selected for processing (11)
  • .skills/compose-ui/strings-index.txt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt
  • feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt
  • feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
✅ Files skipped from review due to trivial changes (1)
  • .skills/compose-ui/strings-index.txt
🚧 Files skipped from review as they are similar to previous changes (10)
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt
  • feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt
  • feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt

📝 Walkthrough

Walkthrough

This PR changes local firmware handling to a select-then-confirm flow. It adds filename resolution, validation, pending-selection state, confirmation UI, updated strings, and expanded tests for bundle extraction, cleanup, and cancellation.

Changes

Local firmware confirmation flow

Layer / File(s) Summary
Localization strings for firmware validation/confirmation
.skills/compose-ui/strings-index.txt, core/resources/.../strings.xml
Adds and reorders firmware recovery/update strings for confirmation UI, missing-target cases, unsupported methods, and file-format requirements.
Display-name resolution per platform
FirmwareFileHandler.kt, AndroidFirmwareFileHandler.kt, JvmFirmwareFileHandler.kt, CommonFirmwareRetrieverTest.kt
Adds getDisplayName to the handler contract and implements URI-based filename resolution on Android and JVM, with test support.
Local firmware filename and pending-file validation
FirmwareFileHandler.kt, IsValidFirmwareFileTest.kt
Adds pending-file metadata and validation types plus filename, suffix, and target checks for BLE, ESP32, and USB local firmware files.
ViewModel pending state and update flow
FirmwareUpdateViewModel.kt
Adds pending local-firmware state, reworks prepare/confirm/dismiss handling, updates cleanup and update-start behavior, and expands local validation error mapping.
Confirmation dialog UI
FirmwareUpdateActions.kt, FirmwareUpdateScreen.kt
Adds confirm/dismiss actions and renders a local firmware confirmation dialog from pending state.
ViewModel confirmation flow tests
FirmwareUpdateViewModelFileTest.kt
Reworks tests around the new two-step flow and covers extraction, cleanup, cancellation, and validation cases.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FirmwareUpdateScreen
  participant FirmwareUpdateViewModel
  participant FirmwareFileHandler
  participant FirmwareUpdateManager

  User->>FirmwareUpdateScreen: pick local firmware file
  FirmwareUpdateScreen->>FirmwareUpdateViewModel: prepareLocalFirmwareFile(uri)
  FirmwareUpdateViewModel->>FirmwareFileHandler: getDisplayName(uri)
  FirmwareFileHandler-->>FirmwareUpdateViewModel: filename
  FirmwareUpdateViewModel->>FirmwareFileHandler: validateLocalFirmwareFileName(...)
  FirmwareFileHandler-->>FirmwareUpdateViewModel: validation result
  FirmwareUpdateViewModel->>FirmwareUpdateViewModel: set pendingLocalFirmwareFile
  FirmwareUpdateScreen->>User: show confirmation dialog
  User->>FirmwareUpdateScreen: confirm or dismiss
  FirmwareUpdateScreen->>FirmwareUpdateViewModel: confirmLocalFirmwareFile() / dismissLocalFirmwareFile()
  FirmwareUpdateViewModel->>FirmwareFileHandler: validatePendingLocalFirmwareFile(...)
  FirmwareFileHandler-->>FirmwareUpdateViewModel: valid or invalid reason
  FirmwareUpdateViewModel->>FirmwareUpdateManager: startUpdate(uri, pendingArtifact)
  FirmwareUpdateManager-->>FirmwareUpdateViewModel: update state

</details>

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 5</summary>

<details>
<summary>✅ Passed checks (5 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                           |
| :------------------------: | :------- | :-------------------------------------------------------------------------------------------------------------------- |
|      Description Check     | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                           |
|         Title check        | ✅ Passed | The title clearly matches the main change: adding an explicit confirmation step before flashing local firmware files. |
|     Docstring Coverage     | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.            |
|     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.                                              |

</details>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

<details>
<summary>🧪 Generate unit tests (beta)</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---




<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>

<!-- tips_end -->
Loading

@github-actions github-actions Bot added the enhancement New feature or request label Jul 5, 2026
@jeremiah-k jeremiah-k marked this pull request as ready for review July 5, 2026 15:06

@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: 1

🧹 Nitpick comments (2)
feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt (1)

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

Rename this test or add coverage for the non-Ready cleanup path
cancelUpdate() clears pendingLocalFirmwareFile, so confirmLocalFirmwareFile() returns at the null guard here and never reaches cleanupPendingLocalFirmwareArtifact(...). This test only covers the no-op-after-cancel case; add a separate case with a still-set pending selection and non-Ready state, or rename it to match the asserted behavior.

🤖 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/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt`
around lines 599 - 623, The test currently named confirmLocalFirmwareFile cleans
up artifact when state is not Ready only verifies the no-op path after
cancelUpdate clears pendingLocalFirmwareFile, so it never exercises
cleanupPendingLocalFirmwareArtifact(...) in confirmLocalFirmwareFile. Either
rename this test to reflect the null-guard/no-op behavior, or add a separate
case in FirmwareUpdateViewModelFileTest that keeps pendingLocalFirmwareFile set
while moving the ViewModel state away from Ready so the cleanup path is actually
covered.
feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt (1)

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

Duplicate target-fallback logic.

effectiveTarget() (platformioTarget.ifEmpty { hwModelSlug }) duplicates the identical fallback already used in JvmFirmwareFileHandler.extractFromZipFile (hardware.platformioTarget.ifEmpty { hardware.hwModelSlug }, feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt:217), and is likely re-implemented again inside the validation logic in FirmwareFileHandler.kt that this ViewModel calls into. Worth consolidating into a single DeviceHardware extension in the common model to avoid future divergence between the value shown to users and the value actually used for matching.

♻️ Suggested consolidation
-    /** pioEnv-aware target: falls back to the hwModel slug when [DeviceHardware.platformioTarget] is unset. */
-    private fun DeviceHardware.effectiveTarget(): String = platformioTarget.ifEmpty { hwModelSlug }
+    // Replace with a shared `DeviceHardware.effectiveTarget` extension defined once in the common model
+    // module, and reuse it from JvmFirmwareFileHandler / FirmwareFileHandler as well.
🤖 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/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt`
around lines 817 - 819, Consolidate the target fallback logic so it is defined
once on DeviceHardware and reused everywhere instead of being duplicated in
FirmwareUpdateViewModel.effectiveTarget,
JvmFirmwareFileHandler.extractFromZipFile, and the validation path in
FirmwareFileHandler. Move the platformioTarget.ifEmpty { hwModelSlug } behavior
into a shared common extension or helper on DeviceHardware, then update the
ViewModel and file handlers to call that shared symbol so the displayed target
and matching logic always stay in sync.
🤖 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/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt`:
- Around line 142-164: The validation order in validatePendingLocalFirmwareFile
should prioritize stale selection detection before rechecking the filename
against the new Ready state. Move the currentTarget/ConfirmationContextChanged
checks ahead of validateLocalFirmwareFileName so that when updateMethod or
address has changed since selection, the function returns
LocalFirmwareFileValidation.Invalid(LocalFirmwareFileValidationReason.ConfirmationContextChanged)
instead of a misleading method-specific filename error. Keep the existing
TargetMismatch handling and only run validateLocalFirmwareFileName after
confirming the context still matches.

---

Nitpick comments:
In
`@feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt`:
- Around line 817-819: Consolidate the target fallback logic so it is defined
once on DeviceHardware and reused everywhere instead of being duplicated in
FirmwareUpdateViewModel.effectiveTarget,
JvmFirmwareFileHandler.extractFromZipFile, and the validation path in
FirmwareFileHandler. Move the platformioTarget.ifEmpty { hwModelSlug } behavior
into a shared common extension or helper on DeviceHardware, then update the
ViewModel and file handlers to call that shared symbol so the displayed target
and matching logic always stay in sync.

In
`@feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt`:
- Around line 599-623: The test currently named confirmLocalFirmwareFile cleans
up artifact when state is not Ready only verifies the no-op path after
cancelUpdate clears pendingLocalFirmwareFile, so it never exercises
cleanupPendingLocalFirmwareArtifact(...) in confirmLocalFirmwareFile. Either
rename this test to reflect the null-guard/no-op behavior, or add a separate
case in FirmwareUpdateViewModelFileTest that keeps pendingLocalFirmwareFile set
while moving the ViewModel state away from Ready so the cleanup path is actually
covered.
🪄 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: da917a8c-b6ab-47ca-ae1b-73dd4146114a

📥 Commits

Reviewing files that changed from the base of the PR and between d8afe84 and efa29bd.

📒 Files selected for processing (11)
  • .skills/compose-ui/strings-index.txt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt
  • feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt
  • feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt

@jeremiah-k jeremiah-k marked this pull request as draft July 5, 2026 15:22
Add filename validation and a confirmation dialog before flashing local
firmware files, preventing users from accidentally flashing wrong-target
firmware. Supports both specific firmware files and release .zip bundles.

Validation flow:
- User picks a file via the system file picker (MIME */*).
- prepareLocalFirmwareFile resolves the file: tries direct filename
  validation first; if the file is a .zip bundle, extracts the matching
  per-target entry via extractFirmware with a per-platform extension
  (.zip for nRF52 BLE DFU, .bin for ESP32, .uf2 for USB).
- The confirmation dialog shows the resolved filename (extracted entry
  name for bundles) so the user verifies the correct target.
- confirmLocalFirmwareFile re-validates against current device state
  before starting the update, catching hardware/connection changes.

State management:
- A stale-state guard makes prepareLocalFirmwareFile a complete no-op if
  the Ready state changes during the suspend call (cancelUpdate,
  checkForUpdates, device disconnect).
- A prepareJob is tracked and cancelled on dismiss/cancel to prevent
  double-prepare temp-file leaks.
- Extracted temp artifacts are cleaned on dismiss, cancel, onCleared,
  and on every error path.

Per-platform extension mapping (localFirmwarePayloadExtension) fixes a
pre-existing bug where ESP32-over-BLE bundle extraction searched for
.zip entries instead of .bin.
Add localized strings for the confirmation dialog, validation error
messages, and archive-missing-target error. Update strings-index.txt.
Add unit tests for filename validation (all platforms), bundle extraction
(nRF52/ESP32/USB), stale-state no-op, filename-unavailable error, temp
artifact cleanup on dismiss, pending re-validation on confirm, and
checkForUpdates clearing pending selection.
@jeremiah-k jeremiah-k force-pushed the enhancement/local-firmware-file-confirmation branch from efa29bd to 0fdba9a Compare July 5, 2026 17:47
@jeremiah-k jeremiah-k marked this pull request as ready for review July 5, 2026 18:36

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed after the force-push. The confirmation-context ordering fix landed (context change now surfaces "reselect" instead of an extension error), and most temp-file orphan paths are cleaned up — thanks. But two confirmed holes defeat the feature's core purpose (flashing the right firmware), so I'm requesting changes. Details inline. Minor: one new string (firmware_update_invalid_local_file_detail) has unescaped "%1$s" quotes while its sibling escapes them — sort-strings.py/lint may flag it.

LocalFirmwareResolution.Invalid(reason = fallbackReason, fileName = fileName)
} else {
val extractedArtifact =
safeCatching { fileHandler.extractFirmware(uri, state.deviceHardware, payloadExtension) }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This extracts with payloadExtension (".bin") and no preferredFilename, so AndroidFirmwareFileHandler falls back to minByOrNull { it.first.name.length } and picks the shorter plain firmware-<target>-<ver>.bin. On pre-2.7.17 bundles that's the merged bootloader+partition+app image FirmwareRetriever's own comment says esp_ota_end rejects — it deliberately prefers the manifest then -update.bin. Reuse that preference chain (pass preferredFilename) so a side-loaded official bundle doesn't extract an un-flashable image that then passes validation and gets confirmed.

}

private fun validateTargetMatch(fileName: String, target: String, fileExtension: String): LocalFirmwareFileValidation =
if (isValidFirmwareFile(fileName, target, fileExtension)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The target match here delegates to isValidFirmwareFile, whose regex .*[-_]<target>[-_.].* accepts delimiter-bounded superstrings: a tbeam device validates firmware-tbeam-s3-core-*.bin as Valid (real colliding pairs in device_hardware.json: tbeam/tbeam-s3-core, t-echo/t-echo-lite, t-deck/t-deck-pro, nano-g1/nano-g1-explorer). Since this feature exists to block wrong-board files, the match needs to require the target be immediately followed by a version/extension delimiter (i.e. anchor the tail), not just appear delimiter-bounded.

}
}

private suspend fun resolveLocalFirmwareBundle(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bundle resolution here runs getDisplayName + a full multi-MB zip extraction with no user-visible progress — the old path set Processing(firmware_update_extracting) but it was removed and the string isn't even imported. The screen sits at Ready during extraction; a user seeing nothing happen may re-tap and spawn a second prepare. Set an extracting/Processing state around the resolve.

// The user selected the file under a different connection (method or address) than the
// one active now — the file may still match the target, but the confirmation context is stale.
currentState.updateMethod != pendingFile.updateMethod || currentState.address != pendingFile.address ->
LocalFirmwareFileValidation.Invalid(LocalFirmwareFileValidationReason.ConfirmationContextChanged)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The context-changed fix only covers same-target/different-transport. TargetMismatch is still checked before this branch, so switching to a different device mid-dialog (new address and target) shows the scary "file does not match " error instead of the intended "connection changed, reselect". Check ConfirmationContextChanged (address/method delta) before the target match.

@jeremiah-k jeremiah-k marked this pull request as draft July 5, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants