Last updated: 2026-07-13
This document preserves benchmark and fault-recovery history. For release decisions, use ota-completion-audit.md, quality-suite.md, and the current release-readiness report. Old firmware versions below are evidence from specific tests, not setup instructions.
- Controller firmware source:
0.1.32 - Signed application payload:
134,612bytes - Factory package:
DoorUnlockerXiao-dfu.zip, DFU manifest0.5with CRC16 - Signed candidate package:
DoorUnlockerXiao-signed-dfu.zip, DFU manifest0.8with P-256 ECDSA - Installed bootloader: an earlier corrected project-signed Adafruit nRF52 Bootloader
0.11.0, dual-bank, S1407.3.0, advertisingDoorDFUStable; signed application transitions through0.1.32have activated over BLE. - Candidate bootloader: SHA-256
600b76172de0d8f7ef624ddee17846efe8fd0f59ce27d36d1134008ad27351b1, recovery build IDb2409e808fefca642042, with a compile-enforced nRF52840 ACL guard for the MBR and bootloader/settings range. - Production-proof status: the exact candidate reported that ID from a physical double-reset USB mount and passed signed serial recovery; production remains blocked on the remaining required interruption and unsigned-rejection campaign.
- Installed wireless update protocol: Legacy DFU init-packet format
0.8with P-256 ECDSA enforcement - Latest fixed-15 iPhone wireless-only proof: three consecutive PRN
9uploads at15,149,16,300, and17,170 B/s; two later unattended-phone transfers measured11,535and10,201 B/s - Historical Mac wireless-only comparison: PRN
9sustained about6 KB/s; PRN0,4, and32each regressed to about3-4 KB/s - Exact
v0.3.0-beta.1Mac proof exposed a client regression:DoorDFUStablemissed an exactDoorDFUpacket-size gate and fell back to 20-byte writes, producing roughly1.2 KB/s(111supload). The gate now recognizes the project-ownedDoorDFU*family in both apps. - Post-fix wireless-only iPhone proof: PRN
9selected a244-byte payload and uploaded the135,495-byte signed package in6.12s, with roughly22-25 KB/saverage progress throughput and post-reboot BLE verification in22send to end. - Post-fix Mac proof: the same shared repair reduced signed upload time from
111sto40s. This confirms the 20-byte fallback was the primary regression; macOS CoreBluetooth scheduling remains slower than the iPhone on the same controller. - Installed signed bootloader negotiated Legacy DFU payload:
244bytes - Measured release tuning: iPhone and Mac PRN
9, object-preparation delay0.3s, fixed15msinterval, automatic PHY, 16 HCI receive buffers, 18 flash-queue entries, and flash local latency50
The current iPhone and Mac path uses a trusted device, a signed BLE OTA-entry command, no controller USB connection, the signed DoorDFU package, reboot, secure reconnection, and a post-reboot firmware-version notification. The earlier factory-bootloader 19s proof remains useful as a historical throughput baseline only.
On July 12, a real controller-power-loss test removed battery power after an iPhone transfer crossed 30 percent. The factory single-bank bootloader did not advertise AdaDFU after power returned; it flashed red in USB DFU mode while the relaunched iPhone correctly retried its journal three times. USB UF2 recovery restored application 0.1.26. The protected controller state remained intact: two trusted devices, lock name College View Door, 30-second timeout, and 95/20 servo angles all survived. The machine-readable baseline is docs/ota-factory-power-loss-baseline.json.
That failure became an explicit architectural gate. The signed dual-bank candidate was installed with the operator's explicit no-SWD risk acceptance. It stages the replacement while the old application remains bootable, defaults an invalid application to BLE DoorDFU, and preserves a deliberate reset-button double press as a recovery path. Real battery cuts at 30% and 80% both returned to normal firmware 0.1.26 without USB recovery and preserved all trusted devices and settings.
iOS and macOS share a durable firmware-update journal. It records the staged package, byte count, SHA-256 fingerprint, target version, phase, attempt count, last progress, and failure reason. The transaction is retained until normal firmware reports the expected version.
After an app relaunch or interrupted transport, recovery is normal-mode first:
- Probe the normal Door Unlocker service/name for four seconds.
- If expected firmware responds, verify it and clear the journal.
- If old firmware responds, use the trusted secure command to re-enter DFU and restart cleanly.
- If normal firmware does not respond, scan for the Nordic/Adafruit bootloader and restart the Legacy DFU transfer from the saved package.
- Retry transient Bluetooth/app/power interruptions at most three upload attempts. Treat CRC, signature, invalid-object, unsupported-package, and package-integrity errors as terminal until a different package is supplied.
The normal-first order matters because the legacy bootloader may reboot the previous valid application when its DFU central disappears. Searching only for the bootloader would leave both apps stuck even though healthy normal firmware had returned.
The Mac stages its ZIP under Application Support with same-volume replacement. Neither app relies on a temporary file surviving relaunch.
Routine product software changes must not require opening the enclosure or connecting controller USB-C. A trusted iPhone or Mac must be able to deliver:
- signed Door Unlocker application firmware;
- signed bootloader replacements, including transport and recovery-policy changes;
- combined bootloader/application packages when a coordinated migration requires them.
Nordic DFU also defines SoftDevice-capable packages, but this project has not yet physically qualified a signed S140 replacement or signing-key rotation on the exact controller. Those are explicit remaining qualification cases, not established production capabilities. Hardware changes, a completely nonresponsive radio, or corruption below the bootloader remain physical-service cases. USB-C and SWD therefore remain recovery fallbacks, not the normal update path.
script/build_secure_bootloader.sh reproducibly builds Adafruit nRF52 Bootloader 0.11.0 for the exact xiao_nrf52840_ble_sense board from the checked-in public key, without requiring the private application-signing key, with:
DUALBANK_FW=1SIGNED_FW=1- invalid applications default to BLE
DoorDFUStable - intentional reset-button double press mounts the read-only
XIAO-SENSErecovery volume and exposes signed USB CDC serial DFU INFO_UF2.TXTreports the deterministic Door Unlocker recovery build ID- project P-256 public key compiled into the bootloader
- raw UF2 writes rejected; signed USB recovery uses serial DFU
- S140
7.3.0/ firmware ID0x0123, matching the XIAO application build 397,312-byte maximum dual-bank application size; the current134,612-byte payload fits with substantial margin- upstream Nordic dual-bank activation and persisted bootloader settings; the removed custom activation journal is not part of the release design
- bank 0 remains bootable during interrupted transfer; activation power-loss behavior remains a required exact-hardware proof
- ATT MTU support raised from the legacy
23-byte path to upstream's247 - maximum Legacy DFU write payload
244bytes, data-length extension, and automatic 2 Mbps PHY negotiation - fixed
15msconnection interval, connection-event extension, and accelerated flash-write pacing
The public key and candidate metadata are checked in at:
docs/firmware-signing-public-key.pemdocs/firmware-signing-public-key.json
The private key is intentionally outside Git:
~/Library/Application Support/Door Unlocker/FirmwareSigning/firmware-signing-key.pem
Back it up in a protected location before migrating hardware. A controller enforcing this key cannot accept future application images signed by a replacement key.
The generated bootloader artifact is ignored under dist/bootloader/. Its expected SHA-256 is recorded in the public manifest. Rebuild it with:
./script/build_secure_bootloader.shscript/flash_xiao_uf2.sh --build-only always creates the factory CRC16 package and, when the private key exists, also creates the signed candidate package. The structural gate requires their application binaries to be byte-identical and verifies the signed package's P-256 ECDSA signature with OpenSSL:
python3 script/check_ota_bootloader_contract.pyThe same gate parses the one-time migration UF2 block by block. It verifies UF2 structure, nRF52 family ID, artifact hash, and the exact recorded address ranges. It fails if any block touches S140, the application, or the reserved Door Unlocker pairing/settings region. It also checks that the reproducible source and public manifest retain the exact high-throughput BLE profile above. A full quality-suite run rebuilds the candidate and requires that reproducibility contract to pass.
docs/ota-bootloader-installed-proof.json records the exact installed artifact rather than accepting a matching version string. A changed bootloader hash invalidates that proof until signed-package acceptance, unsigned rejection, preserved state, both clients, and the physical interruption campaign are rerun. Production mode remains closed while any required exact-device evidence is missing:
python3 script/check_ota_bootloader_contract.py --require-productionThe original 82.68s upload for roughly 134 KB was about 1.6 KB/s. The optimized iPhone run transferred the same application in 7.29s, averaging about 19 KB/s.
Upstream NordicDFU 4.16.0 hardcodes Legacy DFU packets to 20 bytes, so the project vendors it with a focused compatibility patch. Initial 244-byte physical runs uploaded in 9.54-13.89s but failed final CRC because the factory AdaDFU bootloader was given an ECDSA init packet it does not understand. The same binary wrapped in AdaDFU's legacy CRC16 package uploaded in 7.05s and returned Validate Firmware: Success. The client now enables 244-byte writes only for the known AdaDFU name and project-owned DoorDFU* names; unknown bootloaders remain at 20 bytes. Prefix matching is required because recovery-capable releases advertise names such as DoorDFUStable. Package selection happens after discovery: AdaDFU gets CRC16 and DoorDFU* gets ECDSA.
On July 13, an exact-hardware iPhone matrix tested PRN 0, 4, 6, 8, 9, 10, 11, 12, 14, 16, 24, and 32 with the same signed 134,452-byte package. Every supported setting completed and passed post-reboot BLE version verification. PRN 9 was the clear candidate: three consecutive uploads completed in 5.61s, 6.12s, and 6.36s (median 6.12s, best 5.61s, about 22.0 KB/s median payload throughput). PRN 8 took 6.63-7.02s in the immediately comparable runs; PRN 10-32 were slower and more variable. PRN 0, which relies on CoreBluetooth backpressure without target receipt notifications, passed but took 9.54s.
A later exact-hardware rerun on application 0.1.29 exposed the installed bootloader's variable connection interval: PRN 0 uploaded in 12.12s at 11,962 B/s, while PRN 32 uploaded in 12.06s at 11,389 B/s. Since opposite flow-control settings converged on the same payload time while earlier 15 ms selections reached 22-24 KB/s, the remaining throughput limiter was the installed bootloader's 15-30 ms interval negotiation, not iPhone sender pacing.
The signed fixed-15 ms bootloader was then installed wirelessly from the trusted Mac app. Its bootloader-only package identifies the XIAO nRF52840 hardware revision as 52840; this matters because the Adafruit bootloader rejects bootloader or SoftDevice images for any other revision before accepting image bytes. After the wireless bootloader migration, three iPhone PRN 9 uploads passed post-reboot BLE verification at 15,149, 16,300, and 17,170 B/s (median 16,300 B/s). A three-run PRN 8 comparison was slower at a 14,282 B/s median, so PRN 9 remains the production profile.
A longer same-version campaign added two successful PRN 9 transfers at 11,535 and 10,201 B/s. Its third attempt never entered DFU because the unattended physical iPhone had locked and suspended the foreground debug launch; the report contains no bootloader-discovery or transfer event and therefore is an initiation-harness failure, not a throughput sample. Across the five actual fixed-15 PRN 9 transfers, the median is 15,149 B/s.
Exact-hardware bootloader variants isolated the remaining safe tuning choices:
- forcing 2 Mbps PHY regressed three iPhone transfers to an
11,306 B/smedian; automatic PHY remains faster; - fixing the connection interval at
30msregressed three iPhone transfers to an11,137 B/smedian; fixed15msremains faster; - setting flash local latency to
0reduced the Mac PRN9path from roughly6 KB/sto roughly4 KB/s; - increasing HCI/flash queues from
16/18to32/34reduced the Mac PRN9path to roughly3-4 KB/s; - Mac PRN
0,4, and32each measured roughly3-4 KB/s, while PRN9measured roughly6 KB/s.
Every bootloader variant was installed as a signed bootloader-only package over BLE, exercised with a signed application package, and then replaced wirelessly. The controller is back on the exact release-candidate bootloader package: fixed 15ms, automatic PHY, flash local latency 50, and 16/18 queues. USB-C was not used for these migrations.
script/benchmark_ios_ota_matrix.sh now applies a 10,000 B/s gate by default. The summarizer extracts the final NordicDFU average payload rate from every successful run and requires every run, not only the median or best sample, to meet the floor. Override MINIMUM_THROUGHPUT_BPS only for diagnostic matrices; production evidence must retain the default.
An experimental hybrid that mixed CoreBluetooth queue flow control with Nordic PRN flow control failed (9,760 bytes sent while the controller reported 1,952 received). It was reverted byte-for-byte and is prohibited: a transfer must use exactly one flow-control authority. The supported PRN ceiling remains 32; pushing the receipt window above that is not justified because settings 10-32 already regressed and Nordic warns that oversized windows can overflow a target whose flash cannot keep up. iOS and macOS now share the measured PRN 9 default. The configured 0.3s object-preparation delay is ignored by Legacy DFU.
The PRN 9 candidate also passed two exact-hardware recovery runs on July 13. A forced Bluetooth transport loss at 40% recovered and verified over BLE in 38s. Terminating the iPhone updater process at 50%, relaunching it, and resuming from the durable journal recovered and verified over BLE in 40s. These timings include interruption and recovery; they are not clean-upload throughput samples.
Clean iPhone wireless-only proof:
RUN_ID=<run-id> \
./script/verify_ios_ota.sh --wireless-only --target <new-version>App-termination injection at 30 percent:
RUN_ID=<run-id> INTERRUPT_AT_PROGRESS=30 \
./script/verify_ios_ota.sh --wireless-only --target <new-version>Interactive controller-power-loss injection at 30 percent:
RUN_ID=<run-id> INTERRUPT_MODE=controller-power-loss INTERRUPT_AT_PROGRESS=30 \
./script/verify_ios_ota.sh --wireless-only --target <new-version>The verifier normally refuses this physical mode unless the exact signed dual-bank bootloader has an installed proof, its recovery artifact hash matches, and an SWD recovery path is recorded as available. An explicit --accept-no-swd-recovery-risk override exists only for attended testing after the operator accepts the replacement-hardware risk. At the threshold it terminates the updater to freeze a genuinely partial image and opens the reusable SwiftUI physical-handoff assistant. Start countdown speaks the 3-2-1 instructions; Power restored returns control directly to the waiting verifier after the operator has removed controller power for at least two seconds and restored it. The same assistant accepts direct, spoken USB-C and one-/two-press reset requests without exposing a console. A terminal fallback remains available with PHYSICAL_HANDOFF_MODE=terminal.
script/simulate_dual_bank_power_loss.py models every whole transfer percentage and upstream activation interruption boundaries. It verifies the expected bank-safety invariants, but it complements rather than replaces attended exact-hardware power-loss and USB recovery tests.
Reusable attended presets are available for other recovery scripts:
./script/physical_handoff.sh --preset connect-usb
./script/physical_handoff.sh --preset return-to-battery
./script/physical_handoff.sh --preset reset-once
./script/physical_handoff.sh --preset reset-twiceThe helper process blocks the caller until the confirmation button is pressed, so the waiting test resumes automatically without polling chat or exposing a terminal workflow.
The verifier installs the app, starts a real OTA, watches structured DUFirmware events, and injects the requested failure. It passes only after the app receives the target firmware version over BLE. Signed DoorDFU app-termination runs at 30% and 80% passed in 77s and 96s; a 40% Bluetooth transport loss recovered and verified in 81s. The earlier factory 30% controller-power-loss failure remains the baseline superseded by the signed candidate's successful 30% and 80% battery cuts.
The first signed dual-bank candidate was installed on July 12 and behaviorally identified by its fresh DoorDFU advertisement and successful P-256 package validation. A CoreBluetooth cache issue initially presented the old AdaDFU peripheral name; the shared iOS/macOS manager now prefers the live advertisement name. Real battery cuts at 30% and 80% returned to the previous 0.1.26 firmware and preserved controller state. Those runs remain useful transfer-interruption evidence, but a changed bootloader hash always requires a new content-bound physical proof.
Summarize a console trace:
python3 script/summarize_ota_timing.py \
docs/ota-telemetry/<run>-app-console.log \
--output docs/ota-telemetry/<run>-timing.jsonThe same-version --allow-current mode is a script smoke test, not a release proof. A valid release proof must use a newly bumped firmware version so a stale installed image cannot satisfy the verifier.
The signed bootloader migration and power-loss campaign must be attended with USB-C/J-Link recovery available. Use one freshly bumped package per clean proof and retain all telemetry.
Required cases:
- Clean iPhone wireless update.
- App termination during scan/connection.
- App termination around 30 percent and 80 percent upload.
- Bluetooth loss during upload, followed by reconnect.
- Controller power loss during erase/object preparation.
- Controller power loss around 30 percent and 80 percent upload.
- Power loss after validation but before first verified normal boot.
- Corrupt or wrong-signature package rejection while old firmware remains bootable.
- Valid signed package success after the rejection test.
- One clean Mac wireless update using the same shared DFU implementation.
Every passed case must preserve trusted-device keys, lock name, timeout, servo angles, and a bootable previous or new application. Failed transfer attempts must not clear the journal or masquerade as successful verification.
python3 script/quality_suite.py always verifies the signed package and candidate metadata. python3 script/quality_suite.py --firmware-release additionally requires:
- The installed-proof file matches the exact bootloader artifact SHA-256, public key ID, and bootloader version.
- Physical dual-bank rollback has passed.
- An unsigned package was rejected.
- The exact release DFU package passed wireless BLE entry/upload/reboot/version verification.
- App-termination recovery passed on iPhone.
- A clean Mac update passed with the same shared transport settings.
- Pairings and controller settings survived the campaign.
- Two consecutive newly built versions activated and were verified over BLE without controller USB.
- The exact bootloader mounted
XIAO-SENSEread-only and recovered the signed application through USB serial DFU.
Do not mark this goal complete or publish the bootloader migration as production-ready until those gates pass.
- Adafruit nRF52 Bootloader releases
- Adafruit nRF52 Bootloader build/security documentation
- Nordic iOS DFU Library
- Apple Bluetooth accessory design guidance
- Nordic S140 BLE throughput documentation
- Firmware
0.1.28temporarily added application-side inactive-bank preparation andota_staging_ready. Both were later removed so bank ownership remains entirely inside the bootloader; see the July 13 activation incident report. - Correcting the bootloader package hardware revision to the XIAO nRF52840 value
52840resolved that rejection. Signed bootloader-only packages then installed successfully over BLE from a trusted client. - The fixed-15, forced-2-Mbps, fixed-30, flash-latency, and queue-depth variants were all installed and replaced wirelessly. The final controller was returned wirelessly to the release profile: fixed
15ms, automatic PHY, PRN9, flash local latency50, and16/18queues. - Terminal integrity or compatibility failures clear the saved transaction in both clients, preventing a rejected package from restarting after normal firmware reconnects.