Skip to content

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

Merged
jamesarich merged 9 commits into
meshtastic:mainfrom
jeremiah-k:enhancement/local-firmware-file-confirmation
Jul 8, 2026
Merged

feat(firmware): Confirm local firmware files before flashing#6083
jamesarich merged 9 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.
  • Preferred ESP32 -update.bin payloads when extracting official release bundles.
  • Revalidated extracted payloads before confirmation.
  • Preserved exclusions for non-app artifacts.
  • Cleaned up temporary extracted files on dismiss, error, state change, cancellation, 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.
  • Added coverage for cancelled pending-selection behavior.
  • 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 flashing.
    • Improved file selection by showing a readable filename and additional device/update details.
    • Added support for clearer guidance on required firmware file types and update methods.
  • Bug Fixes

    • Better handles missing or unreadable firmware filenames.
    • Improves validation so local firmware files are checked against the selected device and update method.
    • Clears pending selections when the update context changes or is canceled.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

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: 6792a934-fd0d-4840-a2c3-bf11491f00ba

📥 Commits

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

📒 Files selected for processing (2)
  • .skills/compose-ui/strings-index.txt
  • core/resources/src/commonMain/composeResources/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/resources/src/commonMain/composeResources/values/strings.xml

📝 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 update strings for confirmation UI, validation messages, update-method text, and local package 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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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
Contributor

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
@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
@jeremiah-k
jeremiah-k force-pushed the enhancement/local-firmware-file-confirmation branch 3 times, most recently from b14ab0d to 37a6b5a Compare July 8, 2026 03:33
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.
Prioritize stale confirmation context before target and filename checks so device or transport changes ask the user to reselect the file.

Prefer ESP32 -update.bin payloads when resolving side-loaded release bundles, show extracting progress, and tighten target matching to reject colliding board-name prefixes.
Stop cleanupTemporaryFiles from calling cleanupAllTemporaryFiles on
every invocation — the recursive directory delete would prematurely
destroy other temp files (e.g. extracted .uf2 for USB updates) that
live in the same directory. Individual file cleanup now only deletes
the specific artifact; directory-wide cleanup is reserved for
onCleared terminal teardown.

Wrap prepareLocalFirmwareFile's coroutine body in try/catch so
unexpected exceptions from validation or payload helpers surface as
FirmwareUpdateState.Error instead of silently cancelling the
coroutine and leaving the UI stuck.

Consolidate AndroidFirmwareFileHandler on hardware.effectiveTarget
instead of duplicating the platformioTarget.ifEmpty { hwModelSlug }
pattern, matching JvmFirmwareFileHandler.
Wrap the extracting-state block in try-finally so that if
extractLocalFirmwareArchive or validateExtractedLocalFirmware
throws unexpectedly, the Processing state is always restored to
the previous Ready state instead of leaving the UI permanently
stuck on the extracting spinner.
Allow target followed by a firmware marker segment (e.g.
heltec-v3-firmware-2.7.17.bin, heltec-v3.firmware.bin) in
addition to the standard release-style names. The regex now
accepts target + delimiter + (version | firmware) as a valid
continuation, while still rejecting target-prefix collisions like
tbeam vs tbeam-s3-core.
@jeremiah-k
jeremiah-k force-pushed the enhancement/local-firmware-file-confirmation branch from 37a6b5a to 434cf19 Compare July 8, 2026 19:39
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 8, 2026 20:26
@jeremiah-k

Copy link
Copy Markdown
Contributor Author

@jamesarich I pushed a refreshed branch with the local-file edge cases tightened.

Official ESP32 release bundles now prefer the -update.bin payload, target matching rejects prefix collisions like tbeam vs tbeam-s3-core, extraction shows a processing state, stale connection context is checked before target mismatch, and older local filename shapes like target-firmware-version.bin still validate.

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

Traced the core logic (no deadlock / correct temp-file lifecycle & validation respectively) — LGTM. Details in review pass.

@jamesarich
jamesarich added this pull request to the merge queue Jul 8, 2026
Merged via the queue into meshtastic:main with commit 8bfee2a Jul 8, 2026
17 checks passed
@jeremiah-k
jeremiah-k deleted the enhancement/local-firmware-file-confirmation branch July 8, 2026 23:18
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