MetaBoot recovery: scan-screen rescue mode + v10.2 - #28
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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) —MetaWearScannergains a mode switch:.metaWear(default, unchanged behavior) or.metaBoot, which routes Nordic-DFU-service advertisements into a newdiscoveredMetaBootDevicesdictionary 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;MetaBootAdmissionTestspins that invariant.2. MetaBoot flashing (
MetaWearFirmware) — new publicMetaBootFirmwareUpdaterflashes a board that is already in bootloader mode, addressed by UUID alone (noMetaWearDevice, 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 sameBootloaderInterlockas the app-mode path. To enable this, the stage orchestration moved out of theMetaWearDeviceextension into an internalDFUFlashershared by both paths, andMetaBootProbewas generalized to read N DIS characteristics in one connection (surfaced as publicMetaBootDeviceInfo). 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
Sources/MetaWear/MetaWearScanner.swiftclearMetaBootDevices()Sources/MetaWear/MetaBootAdvertisement.swiftSources/MetaWear/Protocol/MWUUIDs.swiftnordicDFUServiceconstantSources/MetaWearFirmware/MetaBootFirmwareUpdater.swiftSources/MetaWearFirmware/DFUFlasher.swiftSources/MetaWearFirmware/MetaBootProbe.swiftMetaBootDeviceInfoSources/MetaWearFirmware/MetaWearDevice+DFU.swiftDFUFlasher; entry points unchangedApps/…/Features/Scan/ScanView.swiftApps/…/Features/Scan/MetaBootDeviceRow.swiftApps/…/Features/Scan/MetaBootUpdateView.swiftApps/…/ViewModels/ScannerViewModel.swiftrefreshMetaBootScan()Apps/…/ViewModels/MetaBootUpdateViewModel.swiftApps/MetaWear/MetaWearApp.xcodeprojMARKETING_VERSION10.1 → 10.2README.mdReviewer testing notes
CI covers everything except the actual flash. To validate on hardware (MMS or MMR on the desk):
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 passMetaBootAdmissionTests— 8 cases incl. the predicate-disjointness invariantswiftlintcleanMetaWearAppTestspassMARKETING_VERSIONresolves to 10.2 via-showBuildSettings