Skip to content

MetaBoot recovery: scan-screen rescue mode + v10.2 - #28

Merged
lkasso merged 5 commits into
mainfrom
feature/metaboot-recovery-mode
Aug 16, 2026
Merged

MetaBoot recovery: scan-screen rescue mode + v10.2#28
lkasso merged 5 commits into
mainfrom
feature/metaboot-recovery-mode

Conversation

@lkasso

@lkasso lkasso commented Aug 16, 2026

Copy link
Copy Markdown
Member

TL;DR

A board whose firmware flash fails (or is interrupted) gets stuck in bootloader mode — it advertises as "MetaBoot", the normal connect flow can't talk to it, and until now the app had no way to see it, let alone fix it. This PR adds a MetaBoot toggle to the scan screen: flip it, tap the stranded board, flash the latest catalog firmware, done. Ships as v10.2.

Architecture

Three layers, each usable independently:

1. Scanner scan modes (MetaWear)MetaWearScanner gains a mode switch: .metaWear (default, unchanged behavior) or .metaBoot, which routes Nordic-DFU-service advertisements into a new discoveredMetaBootDevices dictionary and suspends normal discovery. Mode-switch (either/or) rather than a second concurrent list, so the UI has an unambiguous "rescue mode" state. The list self-corrects: a flashed board keeps advertising on the same peripheral UUID (as a MetaWear now), so freshness culling can never remove its stale entry — instead, an application-mode ad for a listed UUID removes it immediately. The two admission predicates are disjoint; MetaBootAdmissionTests pins that invariant.

2. MetaBoot flashing (MetaWearFirmware) — new public MetaBootFirmwareUpdater flashes a board that is already in bootloader mode, addressed by UUID alone (no MetaWearDevice, no handoff). updateFirmwareToLatest(identifier:) identifies the board via its MetaBoot Device Information service — hardware rev + model number are the only identity a stranded board can offer — picks the catalog build, and applies the same BootloaderInterlock as the app-mode path. To enable this, the stage orchestration moved out of the MetaWearDevice extension into an internal DFUFlasher shared by both paths, and MetaBootProbe was generalized to read N DIS characteristics in one connection (surfaced as public MetaBootDeviceInfo). Behavior of the existing app-mode update path is unchanged.

3. App UI (MetaWearApp) — wrench toggle next to Scan/Stop swaps the whole scan list into a bootloader-mode list (title becomes "MetaBoot", session-only state). Tapping a board opens an update sheet: device identity + target build, one "Flash Latest Firmware" action (latest-only by design, no file picker), live DFU progress, retry on failure. Closing the sheet wipes and rebuilds the MetaBoot list so the just-flashed board doesn't linger.

Files

File Change
Sources/MetaWear/MetaWearScanner.swift Scan modes, MetaBoot routing + self-correction, clearMetaBootDevices()
Sources/MetaWear/MetaBootAdvertisement.swift New — lightweight descriptor for a bootloader-mode board
Sources/MetaWear/Protocol/MWUUIDs.swift New nordicDFUService constant
Sources/MetaWearFirmware/MetaBootFirmwareUpdater.swift New — public flash-by-UUID entry points
Sources/MetaWearFirmware/DFUFlasher.swift New — stage orchestration extracted from the device extension
Sources/MetaWearFirmware/MetaBootProbe.swift Multi-characteristic DIS reads; public MetaBootDeviceInfo
Sources/MetaWearFirmware/MetaWearDevice+DFU.swift Delegates flashing to DFUFlasher; entry points unchanged
Apps/…/Features/Scan/ScanView.swift MetaBoot toggle, mode-swapped list, sheet + clear-on-dismiss
Apps/…/Features/Scan/MetaBootDeviceRow.swift New — wrench icon + "Bootloader" badge row
Apps/…/Features/Scan/MetaBootUpdateView.swift New — update sheet (identify → flash → progress → done/retry)
Apps/…/ViewModels/ScannerViewModel.swift Mode toggle + refreshMetaBootScan()
Apps/…/ViewModels/MetaBootUpdateViewModel.swift New — sheet state machine
Apps/MetaWear/MetaWearApp.xcodeproj MARKETING_VERSION 10.1 → 10.2
README.md New Firmware updates section (API was previously undocumented); scanner modes

Reviewer testing notes

CI covers everything except the actual flash. To validate on hardware (MMS or MMR on the desk):

  1. Connect a board normally → Device Settings → start a firmware update, or send jump-to-bootloader; either way the board ends up advertising as "MetaBoot".
  2. On the scan screen, tap the wrench toolbar button — the list flips to bootloader mode and the board appears with a "Bootloader" badge within a second or two.
  3. Tap it → sheet shows hardware/model/bootloader + the catalog build → Flash Latest Firmware → watch progress through upload/validate/complete.
  4. Dismiss the sheet: the board should NOT reappear in the bootloader list (it may already have vanished mid-sheet once its app-mode advertisement landed).
  5. Tap the wrench again to exit MetaBoot, rescan, reconnect the board normally, and confirm Device Info shows the new firmware revision.

Worth also exercising the failure path: kill Bluetooth mid-flash and confirm the sheet lands on the error state with a working Try Again.

Test Plan

  • swift test (MetaWearTests + MetaWearFirmwareTests + MetaWearPersistenceTests): 1075 tests / 166 suites pass
  • New MetaBootAdmissionTests — 8 cases incl. the predicate-disjointness invariant
  • swiftlint clean
  • App builds for iOS Simulator; MetaWearAppTests pass
  • MARKETING_VERSION resolves to 10.2 via -showBuildSettings
  • Hardware: full rescue walkthrough above (owner, MMS/MMR on desk)

lkasso added 5 commits August 16, 2026 15:10
Boards stuck in bootloader mode (failed/interrupted flash) advertise as
"MetaBoot" with the Nordic DFU service and were invisible to the SDK:
the admission predicate excluded them on purpose because the normal
connect flow can't talk to a bootloader.

Add a scanner-level mode switch (.metaWear default / .metaBoot) chosen
over a second concurrent list so only one class of device is ever
surfaced at a time — the recovery UX wants an unambiguous "you are in
rescue mode" state, not a mixed list. Switching modes clears the other
mode's entries; the underlying CB scan and the ambient caches (names,
RSSI, MAC broadcast, last-seen) keep running in both modes.

The MetaBoot list self-corrects: a flashed board keeps advertising on
the SAME peripheral UUID (as a MetaWear now), so last-seen freshness
can never cull its stale entry — instead an application-mode ad for a
listed UUID removes it on the spot. clearMetaBootDevices() supports
the app's wipe-and-rescan on returning from a flash.

The two admission predicates are disjoint by construction; a test
pins that invariant so the routing gate can't double-vend a device.
A board stranded in the bootloader has no readable application
firmware and no MetaWear command service, so none of the existing
device-actor entry points could reflash it. MetaBootFirmwareUpdater
takes just the peripheral UUID: updateFirmwareToLatest reads hardware
rev + model number from MetaBoot's Device Information service (the
only identity a stranded board can offer), picks the catalog build,
and flashes with the same bootloader interlock as the app-mode path.
No jump-to-bootloader handoff — the board is already there.

Supporting changes:
- MetaBootProbe generalized to read N DIS characteristics in one
  connection; public MetaBootDeviceInfo carries hw/model/bootloader.
  The bootloader version read during the probe feeds the interlock
  directly, avoiding a second CB session against the same peripheral.
- The stage orchestration (flashStages, DFU pass retry, firmware
  parsing, download staging, interlock planning) moves out of the
  MetaWearDevice extension into DFUFlasher so both the app-mode and
  MetaBoot-mode paths share one implementation; the device extension
  now only owns the state checks and the handoff.
New toolbar toggle next to Scan/Stop flips the scan screen into
MetaBoot mode: the list swaps wholesale to bootloader-mode boards
(Remembered/Nearby/Demo hidden — they'd be empty and misleading), the
navigation title changes to "MetaBoot", and the wrench icon turns into
a checkmark so the active state is legible at a glance. Session-only:
rescue is an occasional tool and a persisted toggle would mystify.

Tapping a board opens the update sheet: identify via the MetaBoot DIS,
show hardware/model/bootloader plus the catalog build it would flash,
then a single "Flash Latest Firmware" action with live DFU progress.
Latest-only by design — no file picker; failures show the error with
a retry.

Closing the sheet wipes the MetaBoot list and lets the running scan
rebuild it: a just-flashed board is back in application mode but its
UUID keeps advertising, so freshness alone would never remove the
stale entry. Boards still in the bootloader re-appear in about a
second.
Incremental release carrying the MetaBoot recovery feature. Build
number stays at Xcode's discretion — upload renumbers it and the
repo value isn't authoritative for App Store Connect.
The firmware-update API had no README coverage at all — no mention of
updateFirmwareToLatest anywhere despite MetaWearFirmware shipping as a
product. Add a full section: catalog check + update on a connected
board, explicit-file flashing, bootloader interlock chaining, and the
new MetaBoot rescue flow. Also document the scanner's scan modes
(including why freshness can't cull a flashed board's stale entry)
and add the app's firmware & recovery feature row.
@lkasso
lkasso merged commit 7dd2a5d into main Aug 16, 2026
6 checks passed
@lkasso
lkasso deleted the feature/metaboot-recovery-mode branch August 16, 2026 22:50
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.

1 participant