UltraWideLock v0.4.0

Hardware validation#

CI gates the host-side logic (make ci: the KAT suite, sanitizers, CBMC, the tooling gates) and compile-gates four images in the NCS toolchain container: the CLIENT=1 build (the tightest configuration in the tree), the shipping image (SMP=1 RELEASE=1, what make release wraps), anchorlink (the witness transport, the side gate and the door alarms), and the nRF5340 satellite. Between them that is nine of the thirteen DWM3001CDK application sources, plus the satellite app. It does not build the nRF5340 DK lock, the remaining CDK configurations, the size baseline or anything ESP32, so make regress still compile-gates those on a bench. The release workflow does build the DK and all three ESP32 chips, but only when a release is cut, so those two legs are first compiled at the moment they ship -- which is the argument for a throwaway tag ahead of a real one. What none of that can exercise is the product itself, which runs against a live iPhone.

This checklist is the manual gate: run every applicable item before cutting a release, and record the results table in the release notes. Part of it is no longer manual -- make regress-hil runs the rows marked automated below and writes build/regress-hil/<timestamp>/verdict.txt naming each one:

RowAutomated byStage
CDK-4make regress-hil REGRESS_HIL_ARGS=--selftestuwb-selftest (reflashes the reader)
CDK-5..CDK-8make regress-hilwalkup, via scripts/hitl-run.sh

Everything else here, CDK-9, CDK-10 and CDK-14..CDK-18 included, still needs a person and a phone.

Three hardware paths have recorded bench evidence: the DWM3001CDK, the nRF5340 DK using the legacy Nordic binary with its default ST25R300/RFAL reader, and ESP32-S3. The in-tree credential stack is now the nRF default, but it does not inherit the legacy binary's result. It must pass the nRF checklist before release. A release covering only one target runs that target's rows and records the others as n/a.

ESP32-C5 is built and bundled by the release workflow, but has no hardware validation record. Mark it build-only in release notes until a C5 checklist is defined and passed. The PN532 variant likewise has automated evidence only and does not inherit the ST25R300 checklist result.

DWM3001CDK#

The primary target, and the shortest bench setup there is: one nRF52833 and the DW3110 in the same module, nothing to wire, on-board J-Link OB. No NFC tap path exists here and none can, so there is no equivalent of HV-5. The board carries no reader IC, and the nRF52833's own NFC peripheral is tag-emulation only.

Test setup#

  • A DWM3001CDK on USB, over its on-board J-Link OB. No EVB to seat, no ribbon.
  • make dfu-key, once per clone. Every image on this board is signed and the key is gitignored, so a fresh clone or a new git worktree stops at configure until it has one of its own.
  • A Thread border router the phone already reaches, for the Matter image (make build). For Apple Home plus Home Assistant, import the Apple Thread credentials into Home Assistant and join every border router to that one dataset before sharing the accessory. A make reader image needs neither a border router nor a commissioner.
  • An iPhone with the lock's Aliro key in Wallet. Apple Home mints it during CDK-7; a reader image takes an imported credential instead, per apps/dwm3001cdk-lock/README.md.
  • Console attached with make monitor. It is RTT, not UART, so make nrf-term does not reach this board.

Checklist#

The Recorded column is what this repository has already seen on hardware. It is not a substitute for running the row: a release records the result you got, not this one.

IDProcedurePass criterionRecorded
CDK-1make test on the release commitExit 0, all host KATs passCI gate
CDK-2make dfu-key, then make build RELEASE=1 SMP=1 PRISTINE=1Exit 0; the production image links and fits the 433,664 B app partition and 128 KB RAMyes, 2026-08-16: 397,360 B flash (91.63%), 120,740 B RAM (92.12%), 10,332 B RAM spare
CDK-3make reader PRISTINE=1Exit 0; the reader-only image links and fitsyes: 285,664 B (65.87%), 79,908 B RAM (60.96%)
CDK-4Flash a make selftest build, boot with no phone presentDW3000 raw DEV_ID = 0xdeca0302 on the RTT consoleyes
CDK-5make flash-erase with the release image, then bootClean boot, ECDH self-test: PASS, BLE advertising starts, no faultsyes
CDK-6Connect from an iPhone with a BLE scanner0xFFF2 enumerates with both characteristics and never prompts to pair; the reader-SPSM characteristic reads 00 80 02 01 00 01 01yes
CDK-7Add the accessory in Apple Home with the setup code the build printedCommissioning completes and the lock tile goes liveyes
CDK-8Relock, then approach from well outside ranging distance, phone pocketedWallet animation plays and the bolt opens, with no phone interactionyes: four unlocks in one session, 2026-08-02
CDK-9Walk awayBolt relocks past the hysteresis margin and does not oscillate at the boundaryno recorded result; run it
CDK-10Power-cycle the board, wait for boot, repeat CDK-8Unlock works without re-commissioning or re-provisioningno recorded result; run it
CDK-11Change something in the tree, then make dfu, pressing SW2 when it asksThe delta goes over Bluetooth and the board's flash comes out byte for byte identical to the target image, with a matching CRCneeds a re-run: last passed 2026-08-03 (bca7534, ed1780c) at 17 to 31 s, but that predates the version-2 wire protocol
CDK-12Repeat CDK-11, opening the window from Apple Home's "Turn On Pairing Mode" instead of pressing SW2D10, the blue LED, blinks at 2 Hz while the window is open, and the push is acceptedyes, on a live commissioned lock, for both openers
CDK-13make fota, push the file from a phone with nRF Device Manager's Images tab, then make fota-doneThe board comes back reporting the target image's SHA-256yes, on the commissioned lock (53b2fe1, 8447e91)
CDK-14100 walk-ups, counting the ones that unlock95% or betteropen, never run; the sample so far is single digits
CDK-15Cut the power in the middle of a CDK-11 apply, then restore itThe board resumes at the right step and boots the target imageopen, never run
CDK-16Hold SW2 for 5 s while the application runs, then upload with scripts/cdk-dfu.shThe board warm-reboots into MCUboot serial recovery and accepts the imageSERIAL RECOVERY ITSELF WORKS -- proven 2026-08-27. Entered over SWD with make ota-recovery (writes BOOT_MODE_TYPE_BOOTLOADER to GPREGRET2, resets), MCUboot answered ultrawidelock_smp.py --serial immediately and accepted a full 406,524 B image; the board booted it and reported the expected 8259462cce91ba50. So the bootloader, the UART and the protocol are all fine. What remains untested is the BUTTON entry -- whether a 5 s SW2 hold actually sets the boot mode -- and the Go mcumgr client. This row is now about those two things and nothing else
CDK-17Record a walk-up with the flight recorder, histogram the STS quality index, pick a floor above the noiseULTRAWIDELOCK_STS_QUALITY_MIN is set from data rather than left at 0open, never run. The DWM3001CDK now enforces this gate, so an untuned floor is a door that can refuse to open
CDK-18Walk-up in NLOS: phone pocketed on the far side of the body, and through an interior doorThe gate still publishes a range and the bolt opensopen, never run. One LOS walk-up passed at sts_ok=1, STS index 62, verdict 24, d=107 mm; that is not a calibration
CDK-19In Home Assistant's Thread integration, send the iPhone's Apple Thread credentials, make that dataset preferred, and join the Home Assistant OTBR to itApple and Home Assistant border routers report one Extended PAN ID; no second preferred dataset is createdopen, never run
CDK-20With CDK-7 still live, use the Home Assistant iOS app's Matter > Add device > already in use path and share from Apple HomeHome Assistant completes commissioning; Apple Home, Home Key, and Home Assistant all operate the same lock; three fabrics are present and two slots remainopen, never run
CDK-21Power-cycle the lock and each border router after CDK-20, then operate it from both controllers and walk upBoth controllers rediscover and operate it without re-pairing; Wallet walk-up still succeedsopen, never run
CDK-22Start another share, abort after AddNOC, and wait past the fail-safeOnly the provisional fabric disappears; Apple and Home Assistant remain live and the slot is reusableopen, never run
CDK-23Remove the Home Assistant fabric with Manage fabrics, power-cycle, then share it againRemoval survives reboot, Apple and Home Key remain live throughout, and the freed slot is reusableopen, never run
CDK-24Reproduce an SRP duplicate registration, then leave the border router runningThe lock retries with a fresh service name and becomes CASE-reachable without restarting the border routeropen, never run
CDK-25While Apple Home is live, offer the lock a different Home Assistant Thread datasetCommissioning refuses that dataset without detaching or replacing the working Apple networkopen, never run
CDK-26Cut power once during a fabric commit and once during RemoveFabric, reboot after each cutEach boot loads an old or new valid per-slot record; no torn table, resurrected removal, or damage to another fabricopen, never run

CDK-8 is this target's EV-7, and it is faked the same way: the bolt moving is not a pass. The Wallet animation is, because that is what proves the reader told the phone it granted access rather than just actuating locally.

| CDK-27 | With a second administrator on and a Matter DoorLock peer bound, walk up once and read the RTT log | The peer's bolt moves, and the log reaches the bound lock UNLOCKED | PASSED 2026-08-22, against apps/nrf5340dk-lock on a Home Assistant fabric. Took five runs and five interoperability fixes; docs/matter-binding-bench.md records each fault and its signature. Never run against a commercial peer |

| CDK-28 | After CDK-27, walk away and let the departure gate relock | Both bolts close, and the log reaches the bound lock LOCKED | PASSED 2026-08-22. Home Assistant showed both locks unlocked and then both locked within the same second |

| CDK-29 | Open https://ultrawidelock.com/flash/index.html in Chrome, pick the DWM3001CDK, and connect | The chooser lists the board, and the page names the image it is running before asking for anything | open, never run |

| CDK-30 | Continue CDK-29 on a board whose image the release publishes a delta from, pressing SW2 when the page asks | The delta goes over Web Bluetooth, the board reboots, and the page reconnects and reports the target SHA-256 | PASSED 2026-08-27, in Chrome on macOS, over Bluetooth with no cable. Board read back v0.3.1.0 sha=8613358327a47e00 active=True confirmed=True, which is exactly the delta target. This is the row the whole branch exists for and it had been open since the first commit |

| CDK-31 | Repeat CDK-30 on a board running a build the release ships no delta from | The page says no over-the-air update applies, names the single-slot reason, points at the J-Link, and sends nothing | open, never run. Proven against a fake board in the fotawire suite; never against a real one |

| CDK-32 | Repeat CDK-30 with USB cable picked instead of Bluetooth, on the J-Link port | The same delta goes over uart0 as mcumgr serial frames, the board reboots, and the page reports the target SHA-256 without the port ever closing | identify half PASSED 2026-08-27 over the CLI (make ota-smp-list OTA_SERIAL=auto); board reported sha=000f654e8c031181, byte-identical to what the radio reported for the same board in the same minute. Multi-frame writes PASSED separately: a 384 B payload (420 B frame, 583 B on the wire, 5 lines) arrived whole and was refused with rc=11, the window gate answering correctly. upload half PASSED 2026-08-27 as well: a 7,667 B delta 000f654e8c031181 -> 78224934aa586a84 went over uart0 in 384 B chunks, MCUboot applied it, and the board came back reporting the produced hash and v0.3.0.0. Confirmed over Bluetooth in the same minute. The browser has still not been run against a board at all -- every result here is the CLI |

| CDK-33 | Time CDK-30 and CDK-32 on the same delta | The cable is materially faster; 384-byte chunks against 105 | open, never run. The ratio is arithmetic, the wall-clock is not: per-request latency over a UART is unmeasured |

| CDK-34 | With the application running normally, pick reinstall everything and try to write | The page says the application answered rather than the bootloader, writes nothing, resets nothing, and does not tell anyone to press SW2 | open, never run. Covered against a fake board; the point is that rc=11 means something different on this path and must not be read as "open the window" |

| CDK-35 | Hold SW2 for 5 s, then pick reinstall everything and write the published whole image | MCUboot accepts ~400 KB over uart0, verifies it, and the board comes back on the published SHA-256 | the transport half PASSED 2026-08-27 over the CLI: 406,524 B at 384 B/chunk into MCUboot, ~4 minutes, board back on 8259462cce91ba50 with v0.3.1.0. No longer gated on CDK-16. Still open only as a BROWSER test -- the page's recovery flow has never been run, and it asks for the SW2 hold rather than SWD, so it also depends on the button half of CDK-16 |

| CDK-36 | Erase the application only (leave MCUboot), then run CDK-35 with no button press at all | CONFIG_BOOT_SERIAL_NO_APPLICATION=y leaves the board already waiting in recovery, and the page installs onto a board with no working software | open, never run. This is the row that decides whether the probe is genuinely optional after the first flash |

| CDK-37 | On a commissioned lock, enumerate the whole GATT table from a Mac (BleakClient(dev) with no services= restriction, which is the walk Chrome performs) | Three services enumerate -- native DFU, credential 0xFFF2, SMP -- with no CBError 8 and no 0xFFF6 | PASSED 2026-08-27. The same probe against the previous image failed at ATT handle 13, the Matter C1 characteristic, and poisoned the host cache so that even restricted SMP discovery failed afterwards. matter_ble_publish() now withdraws 0xFFF6 whenever the advert carries the credential payload. Confirmed in Chrome the same day, which is what the row is really about: the flasher page connected over Web Bluetooth, completed discovery, and read 344da6f022364f7f back off the board -- every attempt before the fix stalled at "looking up the update service" |

| CDK-38 | Repeat CDK-37 with a Matter pairing window open, or on an uncommissioned board | 0xFFF6 is published and the unrestricted walk fails again with CBError 8 at C1 | open, never run. This is the negative control: it proves the fix is the gating and not something else that changed. It is also the case the page still warns about on macOS |

| CDK-39 | With a commissioned lock, run CDK-30 for real in Chrome on macOS | The delta goes over Web Bluetooth and the page reports the target SHA-256 | PASSED 2026-08-27, both halves. Chrome connected, discovered and identified the board, then installed the 344da6f022364f7f -> 8613358327a47e00 delta over Web Bluetooth. Verified afterwards over the cable, not by asking the page: the board reports v0.3.1.0 sha=8613358327a47e00 confirmed=True. Same row as CDK-30, kept separate because this one is specifically about macOS, where every attempt before matter_ble_publish() stalled in discovery |

CDK-14 through CDK-26 are the open rows, and none has ever been run to completion. CDK-16, CDK-17 and CDK-18 cover recovery, STS quality and NLOS walk-up. CDK-19 through CDK-26 are the Apple Home plus Home Assistant release gate added with the five-fabric transaction work: host tests and the target build cover their local state machines, but none inherits a hardware result from those tests. Do not describe multi-admin operation as hardware-robust until all eight of those rows pass. CDK-27 and CDK-28 are the binding, and both PASSED on 2026-08-22 -- which incidentally settles CDK-20 in passing, since a second administrator had to exist before anything could write a binding at all. Note what that does NOT settle: the binding was proven against this repo's own DK, and every one of the five faults it took to get there was a place the code agreed with itself and not with another implementation. A commercial peer has still never answered. CDK-14 is the only rate on this list: everything above it has been demonstrated at least once, and none of it at a rate. CDK-15 is the resumable apply, whose step counter is exercised by design and by host test but has never met a real power cut.

CDK-29 through CDK-31 are the browser update path, and they are the cheapest open rows on this list to close: the firmware is the same SMP=1 release image CDK-13 already passed by hand, so the only thing that has never met a board is the JavaScript. CDK-31 is the one worth running even though it sounds like a non-event -- a board with no applicable delta is the normal state of anything more than a few releases old, and a page that handled it badly would be the first thing most people saw.

CDK-32 through CDK-36 are the cable. CDK-32 and CDK-33 are low risk and cost one build: the transport changed, nothing above it did, and a host suite already compares the framing byte for byte across three implementations -- the standard library's CRC-16/XMODEM and base64, the CLI client, and the browser's copy.

None of these rows needs a browser. ultrawidelock_smp.py --serial speaks the same protocol from the command line, which is the cheaper way to run them and the one that isolates the firmware:

sh
make flash SMP=1 RELEASE=1               # the image with the UART transport in it
make ota-smp-list OTA_SERIAL=auto        # CDK-32, the identify half
make ota-smp      OTA_SERIAL=auto        # CDK-32, the upload half

Running the CLI first is worth the extra step rather than a detour. If it works and the page does not, the fault is in JavaScript; if neither works, the fault is in the firmware or the wire, and no amount of reading the page will find it. That split is otherwise expensive to make.

It has already paid for itself. On 2026-08-27 the first CLI run answered on the first attempt, which settled three things at once that were going to be argued about separately: that the transport works, that the two paths really do reach one implementation (both reported sha=000f654e8c031181 for the same board, minutes apart), and -- because the application receives 127-byte back-to-back mcumgr frames on this exact UART without losing any -- that CDK-16 cannot be blamed on the wire.

The upload half followed, and closes the row: make ota-window to open the update window over SWD (so no button press was needed), then make ota-smp OTA_SERIAL=auto. 7,667 B of delta in 384 B chunks, staged, reset, and the board back on 78224934aa586a84 about half a minute later -- the hash the delta was built to produce, not merely a hash that changed.

That run also proved the thing a synthetic test cannot: that the window gate, the signature check, the receiver, MCUboot's applier and the deployed-record bookkeeping all still work when the bytes arrive over a cable instead of a radio. None of that code knew which it was.

Three more measurements from the same session, all worth keeping:

  • A 600 B payload, which overflows CONFIG_MCUMGR_TRANSPORT_UART_MTU=512, is answered with complete silence: no error, no reply, nothing. That is why the chunk sizes in web/flasher/serial.js sit under their budget rather than at it, and it is now a measured fact rather than an expectation. The board recovers on its own; the next request is answered normally.
  • Every board built before 2026-08-27 reports v0.0.0.0, because CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION was never set and Zephyr's default is 0.0.0+0. It is taken from the repository's VERSION file now, so the image list finally carries something a person can read: v0.3.0.0. The SHA-256 was and remains the authoritative identity -- this is about the field an operator actually looks at.
  • EVENTS_RXDRDY=1 and ERRORSRC=0x1 is recorded in scripts/cdk-dfu.sh under "verified WORKING". On nRF52 ERRORSRC bit 0 is OVERRUN, so that line is evidence that bytes arrived AND were dropped -- not evidence that RX is healthy. It is filed under the wrong heading. Whether it is CDK-16's cause is now less likely given the result above, but it should not keep sitting in the "ruled out" column.

CDK-35 and CDK-36 are the ones that matter, and CDK-35 should be run before anything else on this list is attempted, because of what it can settle. It has a command-line form too -- ultrawidelock_smp.py --serial PORT --chunk 128 against a board held in recovery -- which is the same second opinion without the browser in the way. CDK-16 has been open since 2026-08-02 with a symptom nobody has explained: MCUboot sits in its recovery window on a UART that is measurably working, and does not answer. Everything ruled out so far was ruled out from the board's side. The browser is a SECOND, INDEPENDENT HOST IMPLEMENTATION of the same protocol on the same wire -- so if it gets an answer, the fault was never the board, and if it does not, the fault is not the Go client. That is a real bisection of a stuck bug, available for the cost of one page load, and it is the reason CDK-35 is worth running even though CDK-16 is expected to fail.

CDK-37 through CDK-39 are the Bluetooth path on macOS, and CDK-37 is the only row here that was found by measurement rather than designed. The board published the Matter commissioning service unconditionally while advertising as a credential reader -- the GATT table did not follow the advert -- and macOS reserves those UUIDs, so CoreBluetooth refused descriptor discovery on C1 and Chromium's backend never completed discovery at all (crbug.com/609844). Every Web Bluetooth client on that host lost the board, firmware updates included, and because a refused discovery poisons the host cache it looked intermittent rather than absolute. CDK-38 is the negative control and is the only one of the three still open. CDK-37 was measured with bleak, which shares CoreBluetooth with Chrome but is not Chrome; CDK-39 then closed it properly, ending with a board that had taken a firmware update from a web page over the air with no cable attached. Until CDK-38 runs, the gating is the LIKELIEST cause of that rather than the proven one: something else changed that day too, and a negative control is how you tell.

One thing CDK-39 taught that no test asked for: the image embeds a build timestamp, so make fota-done after a rebuild reports the board is running something else -- and it is right. See the note on IMAGE_VERSION in mk/cdk.mk.

CDK-36 is the claim the page now makes to first-time owners in as many words: that the J-Link is needed once and then never again. Until it has been run, that sentence is a design intention rather than a measured fact.

What 2026-08-27 settled about CDK-16#

It had been open since 2026-08-02 as "MCUboot sits in its window on a working UART and does not answer". That description was wrong in a way that kept the investigation pointed at the wrong half.

make ota-recovery was added to separate the two halves, because every previous test exercised both at once. It writes BOOT_MODE_TYPE_BOOTLOADER (0x01) to GPREGRET2 at 0x40000520 over SWD and resets -- exactly what the application's button handler does -- so the ENTRY is performed by the probe and the SERIAL is then tested on its own.

MCUboot answered on the first attempt, and took a whole 406,524 B image. The board booted it. So:

verdict
the UART, pins, VCOM, 115200, legacy nRF driverfine -- the application had already shown this
MCUboot's serial adapterfine -- it answers and accepts a full image
the mcumgr protocol over this wirefine, three implementations agree
the 5 s SW2 hold setting the boot modeuntested, and now the prime suspect
the Go mcumgr binaryuntested against a known-good bootloader, and the other suspect

The next measurement is a one-liner and needs a person: hold SW2 for five seconds, then run make ota-smp-list (Bluetooth). If a board is found, the application is still running and the hold never entered recovery -- which would make CDK-16 a button-and-retention bug that has been mistaken for a serial one for three weeks. If no board is found, the hold worked and the Go client is what failed.

Note also that MCUboot's ceiling is HIGHER than the application's, not lower: CONFIG_BOOT_SERIAL_MAX_RECEIVE_SIZE is 1024. The 128-byte chunk this project used for recovery was a guess dressed as caution, and it cost about nine minutes on a 400 KB image -- measured at ~500 B/s against ~1.6 KB/s for 384.

nRF5340 DK#

Test setup#

  • nRF5340 DK with DWM3000EVB (Arduino header) and X-NUCLEO-NFC12A1, wired per apps/nrf5340dk-lock/overlays/dw3000-nfc.overlay.
  • make dfu-key, once per clone, the same key the DWM3001CDK uses. DFU=1 is this board's default and the build refuses to give MCUboot a key it does not own. DFU=0 builds the older no-bootloader layout and needs none.
  • An iPhone (or Apple Watch) with the lock's Aliro key provisioned in Wallet.
  • Serial console attached (make nrf-term) to observe logs.

Checklist#

IDProcedurePass criterion
HV-1make test on the release commitExit 0, all host KATs pass
HV-2make rebuild (pristine)Exit 0, image links and fits flash
HV-3Flash a make selftest build, boot with no phone presentBoot self-test reports pass on the console
HV-4Flash the release image (make flash-erase for a first flash), bootClean boot, credential source stack enabled appears with no errors, BLE advertising starts
HV-5Tap the phone on the NFC reader (Express Mode, screen off)Lock actuates to unlocked; console logs the granted access
HV-6Relock, then approach from well outside ranging distance, phone pocketedLock unlocks on approach with no phone interaction
HV-7Walk away from the lockLock relocks after passing the hysteresis margin, and does not oscillate at the boundary
HV-8Power-cycle the DK, wait for boot, repeat HV-5 and HV-6Both unlock paths work without re-provisioning the key

ESP32-S3#

No NFC tap path exists on this target, so there is no equivalent of HV-5.

Test setup#

  • ESP32-S3 dev board with a DWM3000EVB wired per docs/esp32-bringup.md, including the EVB's power-select jumper.
  • An iPhone with a key provisioned in Wallet for this reader identity. A key minted against a different reader will not authenticate.
  • Serial console attached (make monitor from apps/esp32-matter-lock).

Checklist#

IDProcedurePass criterion
EV-1make check on the release commitExit 0, all host suites pass
EV-2make rebuild in apps/esp32-matter-lockExit 0; verify_port.sh reports the link seam intact and the app fits its partition
EV-3make flash-erase, then bootClean boot, onboarding codes printed, no watchdog resets
EV-4Commission into a home with the printed codeCommissioning completes; status shows the fabric
EV-5Confirm a key lands in the phone's walletKey appears, tied to this reader
EV-6ultrawidelock prov on the consoleReports a provisioned identity, not the dev-identity fallback warning
EV-7Approach from well outside ranging distance, phone pocketedWallet unlock animation plays and the bolt opens, with no phone interaction
EV-8Watch the console through EV-7Continuous positive distances tracking the approach; no watchdog reset
EV-9Walk awayBolt relocks past the hysteresis margin and does not oscillate at the boundary
EV-10Re-approach within the same sessionUnlocks again without a reconnect
EV-11Power-cycle the board, wait for boot, repeat EV-7Unlock works without re-commissioning or re-provisioning
EV-12lab on, then approach from beyond BLE range and watch the tracegate.hold appears and no rrx/rtx follows until gate.open; the radio stays dark while the phone is far
EV-13Loiter out of range for ~10 s during EV-12Repeated session.start / gate.hold / session.end cycles are correct, not a fault; the phone gives up at ~1.9 s and retries
EV-14Unlock, then stand still at the door for 10 sNo relock.sent, bolt does not cycle. iOS pauses ranging when still; a relock here is the regression
EV-15Unlock, then leave brisklyrelock.sent appears before session.end, and the phone shows locked as you go
EV-16Re-approach after EV-15No relock.sent between ph.apc and the grant, i.e. the Wallet does not flash locked then unlocked
EV-17Score any capture that reached UWB-active. The bench scoring script this used is not in this repositoryThe order check passes. ph.m1 before ph.m2 and ph.m3 before ph.m4rx: setup stamps follow message identity, not arrival order
EV-18Read the ranging setup: line of that reportReads rrx SUPPL id=0, rrx IRS, rtx M1, rrx M2, rtx M3, rrx M4. A bare rrx id= with no protocol means pre-fix firmware
EV-19Double-click the board's button while the application runsThe update window opens, and the long press still opens a commissioning window rather than thisopen, never run
EV-20With the window open, install from https://ultrawidelock.com/flash/index.html in ChromeThe whole ~2 MB image goes over Bluetooth, the board reboots into it, and it commissions and unlocks as beforeopen, never run. The entire ESP32 update path is new firmware that has compiled and never executed
EV-21Cut power partway through EV-20, then restore itThe board boots the image it was already running; otadata was never pointed at a half-written slotopen, never run
EV-22Install an image signed with a different keyThe board refuses it with the signature error and writes nothingopen, never run
EV-23Install an image that links but panics before the end of app_mainThe bootloader rolls back to the previous slot at the next boot, and the lock still answersopen, never run. This is the row that decides whether a bad update is recoverable without a cable

EV-7 is the row that matters most and the one most easily faked: the bolt moving is not a pass. The Wallet animation is the pass criterion, because that is what proves the reader told the phone it granted access rather than just actuating locally.

EV-12 to EV-16 gate the RSSI power gate and the relock policy. The guide that explained each measurement and its thresholds documented bench tooling that is no longer in this repository, so it is not included here. EV-14 and EV-16 are regression rows: both behaviours were shipped broken once and are invisible unless specifically looked for.

EV-17 and EV-18 are the third such row. The ranging-setup latency stamps used to be assigned by arrival order, and the phone sends a proto-3 (supplementary-service) SDU ahead of Initiate-Ranging-Session, so every device-to-reader label sat one frame early — the report claimed M2 arrived before M1 was sent. Nothing in the protocol depended on it, but every setup timing read from those captures was wrong. Measured on the fixed firmware, the setup exchange is IRS +2.0 ms M1, +27.8 ms M2, +2.4 ms M3, +27.7 ms M4; the old labelling reported that as a 29.7 ms IRS-to-M4 span, which was really IRS to M2.

EV-19 through EV-23 are the over-the-air update path, and none of it has ever executed: unlike the DWM3001CDK, which already spoke mcumgr, this is entirely new firmware. EV-23 is the row that decides whether the feature is safe to have at all. An update that links and then panics is not hypothetical, and without a working rollback the recovery for it is a cable -- which is the one thing the whole path exists to avoid. EV-21 and EV-22 are the other two ways it can go wrong quietly: a half-written slot that the bootloader is nonetheless pointed at, and an image that was never signed by the release key.

Recording results#

Copy the relevant tables into the release notes with a Result column (pass / fail / n/a), plus: firmware commit hash, toolchain version (NCS, or ESP-IDF and esp-matter), board revision, phone model, and iOS version. A release ships only when every applicable row is pass.