Skip to content

nRF Redesign — open issues

Status: working document, not normative. The spec is firmware_spec_v3_nrf.md; this is everything that spec had to assume, decide, or leave open in order to exist. Created: 2026-08-09, from drafting v3 §0.1. Companion: Safety.md — the hard-override redesign, which is one of the issues below and large enough to have its own file.

Nothing here is a task list. Actionable items live in Todos.md; this is the reasoning behind them, and the things that are too small or too conditional to be todos but would be expensive to rediscover.

Each entry: what it is, why it matters, what would settle it.


A. Could change the architecture

These are not tuning. If any of them resolves the wrong way, part of v3 is void.

A1. ~~The BM15C fallback may delete the reason for the whole pivot~~ — resolved 2026-08-09, it does not

Resolved. The datasheet is filed (datasheets/BC15M+Product+Specifications.pdf) and the fallback module is built on the Nordic nRF54L15, which lists Channel Sounding in its specifications. The fallback keeps channel sounding, v3 §4 survives it, and the proximity engine stays deleted in every scenario. This was the largest open risk in the redesign and it closed the good way.

But the fallback is not free, and two of its costs are now specific rather than vague:

BM20C (primary) Fallback (nRF54L15)
Non-volatile 2036 KB RRAM 1524 KB flash
RAM 512 KB 256 KB
Channel sounding yes yes
WiFi companion WM02C/E none — cannot drive it

RAM halves. That is the constraint that started this move: the 2026-08-04 email to Fanstel says "I currently use the BM15C, but I am memory constrained on this device." Falling back means falling back into the problem the pivot was partly meant to escape — and flash headroom on the ESP32 build is already ~80% (CLAUDE.md). A fallback build needs a real footprint budget, not an assumption.

Naming, worth fixing before anything is ordered or quoted: the filed datasheet is titled BC15C (the file is named BC15M) and mentions BM15C three times against 56 for BC15C. Timeline.md BLK-15 and launch-plan.md §5 now both say BC15C with the discrepancy flagged, but the document is not the thing that gets shipped — the order is. Confirm which part number is the footprint-compatible nRF54L15 module before placing one. Fanstel's BC15C / BC15M / BC15E / BC15P / BM15C / BM15M / BM15E naming is dense and easy to get wrong by one letter.

A2. Channel sounding's API shape is assumed, not verified

What. v3 §4.3 specifies phase-based ranging as primary with CS-RTT as a sanity check, and treats disagreement beyond a tolerance as a failed measurement. That pairing is conventional — PBR gives resolution, CS-RTT is bounded by the speed of light and so is far harder to spoof — but it is written against the feature in general, not against the NCS version that will be pinned in west.yml.

Why it matters. It is the single most load-bearing assumption in v3 and the most likely to be wrong in detail. Everything in §4.4 and §4.5 sits on top of it.

What would settle it. Build chunk A against a pinned NCS, read what the distance-estimation API actually offers on the nRF54LM20A, and revise §4.3 before chunk G starts. Check this first, ahead of any other measurement.

A3. Whether RESET (S1) can be read as an ordinary input

What. Board V1 has one user button (P0.05) plus S1 on the module's RESET pad. On some Nordic parts the reset pad is remappable to GPIO via UICR; on others it isn't. Unverified for the nRF54LM20A.

Why it matters. It bounds the hard-override design space (Safety.md). Less than it looks, though — the replacement mechanism in Safety.md is deliberately designed not to need a second button, precisely so the answer doesn't gate BLK-01.

What would settle it. The nRF54LM20A datasheet's UICR section.

A4. Whether v1 ships with WiFi at all — new, 2026-08-09

What. The 2026-08-09 log makes the case for dropping the WM02C from the first run: the gym-arrival check could be answered by the phone's GPS without breaking the root-of-trust model (uninstalling the app doesn't help — a missing report is not a compliant one), the BC15C fallback becomes viable, certification gets simpler, and BOM falls from ~$15 to ~$10 on both the watch and the anchor. The log lands on "right now im leaning towards no, i still do want the WM02C", conditional on there being no other way to close the out-of-range bypass.

Why it matters. The bypass is real and the log states it precisely: leave the watch two floors down, out of BLE range, and go back to bed. Out of range is indistinguishable from away, and away is compliant, so Sunrise Lock silently stops. It only exists in houses big enough to have an out-of-BLE-range room — which is a real slice of the beachhead audience and not the dorm-room slice.

What is actually true, and it is not what the log assumes. The log says "the watch has no way to tell the anchor… that it has been taken off" and "i dont think thats possible" without WiFi. The mechanism already exists and already ships. v2 §5.2 detects removal from the IR sensor and §5.4.4/§5.5.1 send WATCH_REMOVED to the beeping anchors — over UDP. send_to_anchors() refuses to run without WiFi and says so in the log line (WatchFIrmware/src/main.cpp:1782). So this is not an unsolved problem to be weighed against WiFi's cost; it is the problem WiFi is already solving, and dropping the module un-solves it. Recorded in v3 §1.5.

That reframes the decision rather than settling it. A second out-of-BLE-range transport is what's required, and WiFi is the only one on the board. The alternatives are all worse or unavailable: BLE mesh through other anchors needs anchors in between, a sub-GHz radio is a new part and a new grant, and the phone can't carry it because the phone is not trusted to be present.

Two things this also touches, which the log doesn't mention. The anchor gets its time from SNTP and will not beep without it (v2 §4.7), and the watch-side anchor repair path (§5.5.3) exists specifically because a stranded nightstand anchor makes Sunrise Lock fail silently. Both die with WiFi. The BOM saving is real, but it is a saving on the two features that make the flagship template trustworthy.

What would settle it. A decision, not a measurement — it is a product question. The one piece of evidence that would help is how many of the beachhead audience have a room out of BLE range at all; the honest answer is that nobody knows, and the failure is silent when it happens.

✅ Settled 2026-08-11 — v1 ships with the WM02C. "I am going with Wifi. It simply sacrifices too much on this product's mission to not have Wifi." Decided without the evidence above, and correctly so: the "what is actually true" paragraph converted the question from is the bypass worth $5 into is a known bypass of the flagship template worth $5, and once framed that way the missing prevalence number stops being load-bearing. It would have had to come back very low to change the answer.

What this does not settle, and now inherits the weight. WiFi's SAR problem is no longer a contingency — see A-series notes and FCC Certification.md §5. Shipping the module means a SAR test or an ~8 dB declared-power cut, and the range that cut spends is the range WATCH_REMOVED needs to cross two floors. The decision above is only as good as that range holding, which makes "measure the actual WiFi range of WATCH_REMOVED on the ESP32 build" the highest-value measurement in this document — it is cheap, it is available today on hardware that exists, and it is the input to a five-figure decision. Recorded in v3 §1.5 and launch-plan.md §5.


B. Numbers that were invented, with reasoning

Every one is a defensible starting value, not a guess dressed as a fact, and every one is in v3 §11 with what would overturn it. Repeated here in priority order, because §11 is a table and priority is the thing a table can't show.

B1. CS_ABSTAIN_MAX_CONSECUTIVE = 5 — the one I'd check first

What. How many consecutive failed or low-quality measurements before the watch stops holding its verdict and resolves to the criterion-dependent fail-safe.

Why it matters. It is a security boundary, not a tunable. Abstention has to be free enough that a wall doesn't punish a compliant user, and expensive enough that "put your hand over the watch" isn't a bypass. Too high and jamming is free; too low and normal edge-of-range use fires false enforcement — which launch-plan.md §12.30 identifies as the failure that kills you in a reviewer's hands.

What would settle it. Bench data on how often CS legitimately fails at the edge of usable range, with a body and furniture in the path. Until then, err toward the lower number: a false alarm is recoverable, a free bypass is not.

B2. ~~CLOCK_GUARD_S = 30 — the one with the nastiest failure mode~~ — withdrawn 2026-08-09

Withdrawn. This rested on the belief that Board V1 had no 32.768 kHz crystal. The BM20C datasheet says both crystals are embedded in the module, so LFCLK can run on a crystal and the drift problem does not exist. The Sunrise Lock silent-skip failure mode is not live. CLOCK_GUARD_S drops to 5 s and stops being a number that needs measuring. v3 §3.2 rewritten; §11 loses two rows.

What survives, much smaller: Zephyr must be told to use LFXO. It picks its low-frequency source from devicetree and falls back to the internal RC when nothing says otherwise — so a board definition that omits it runs on RC with the crystal sitting unused and nothing reports a problem. Set it in chunk A; confirm it at runtime in chunk D rather than trusting the build.

Kept rather than deleted, because the mistake is the useful part: this entry was wrong because it treated a recollection — "the only optional thing I skipped was the sleep crystal" — as a hardware fact, without a datasheet. That is the failure mode this workspace's own rules exist to prevent, arriving through a source the rules never named. A note from the author is a report, exactly like a daily log. The datasheet is the fact.

B3. NEAR_ENTER_CM = 200 / AWAY_ENTER_CM = 350, dwells 2 / 4

What. The hysteresis band and the dwell counts that turn a distance into a verdict.

Why it matters. The thresholds are ordinary tuning and will move. The 2:1 dwell ratio is not — see C2.

What would settle it. A walk-in/walk-out corpus in a real room. Expect the band to want widening, not narrowing.

B4. The rest

CS_QUALITY_MIN (SDK default), CLOCK_SHOW_MS = 5000 (pure ergonomics, decide it on a wrist), CS energy per procedure, and board sleep current in DORMANT_SLEEP. The last two both feed BLK-04's battery-versus-example-day contradiction and neither can be estimated usefully from paper — note the datasheet's 2.9 µA System-ON-IDLE figure is the SoC's, and the board's number also carries the LED ring, the IMU, and the WM02C's leakage with VDDIO gated off.


C. Decisions taken that are open to argument

Made deliberately, recorded so they can be reversed cheaply rather than re-litigated from scratch.

C1. Role selection is compile-time, not runtime

One PCB invites one firmware image with a runtime role flag. Rejected: the watch is the root of trust, and a runtime-settable role means a watch can be told it is an anchor — and anchors don't enforce. That reduces the whole of v2 §9 to one settings byte. The bar for defeating a commitment device should be reflashing over SWD with the case open, not a write to NVS.

Costs two firmware images and an SKU that must be flashed correctly. Reversible at v3 §1.3.

C2. The AWAY/NEAR evidence asymmetry survives the pivot

Concluding AWAY needs twice the dwell of concluding NEAR. This looks like leftover RSSI machinery and isn't: physical attacks on a radio are overwhelmingly subtractive — shielding, blocking, a body, a hand — and subtraction can fabricate "far" or "no reading" but can never fabricate "near". So the two conclusions don't deserve equal trust, and the conclusion an attacker wants must be the harder one to reach.

Marked a floor, not a tunable: raise both dwells together if needed, never invert the ratio. v3 §4.4; reasoning in agent-notes.md, 2026-08-09.

C3. Widening a per-anchor distance threshold is a loosening change

Calibration-v2 is replaced by a per-anchor threshold override (v3 §4.7), because "near the nightstand" and "near the front door" are genuinely different distances. But a threshold the user can widen instantly is a one-tap bypass of every stayNear commitment they hold — so widening is classified as loosening under v2 §9.1 and quarantined 24 h like any other. Narrowing applies immediately.

This is the whole of calibration-v2's security story, in three lines instead of a document.

C4. A failed connect is an abstention, not an AWAY measurement

v2 learned this the expensive way: routing connect failures in as AWAY evidence let a stationary watch just out of range march its posterior −610 → −1378 → −2146 → −3473 to saturation on one observation repeated — the exact frozen-evidence failure the engine existed to prevent, arriving through a different door. v3 §4.5 counts a failed connect as one abstention against B1's budget and nothing more.

C5. The 600 s STILL poll tier is deleted

It existed because a motionless wrist produces no new small-scale fading draws, so polling harder bought nothing. That constraint doesn't apply to a time-of-flight measurement. Deleting it costs battery. Flagged in v3 §11 as worth re-deriving from measured CS energy once numbers exist — a third tier may well come back, on different grounds.

C6. The pin map moved rather than being copied

v2 §1.3's table now lives at v3 §2, with a pointer left behind. Two pin tables for one board is precisely the drift this workspace keeps paying for. Reversible at firmware_spec_v2.md §1.3.


D. Work this creates elsewhere

D1. The app loses five characteristics — lockstep

Four on the anchor (prox vector, prox score, fingerprint ctrl, fingerprint data) and the watch's calibration control. MOBILE_APP_SPEC.md §0 needs the lockstep flag the firmware spec's own §0 rules require, and §8.5's guided walk-around calibration screen loses its purpose — it should shrink to a "set the distance for this anchor" control, not be repurposed.

Everything else in the contract is deliberately frozen, including SCHEDULE_FORMAT_VERSION = 0x02. That's a design goal, not luck: the app is on the critical path (launch-plan.md §12.20) and every byte of moved contract is app work competing with the campaign.

D2. ~~CLAUDE.md is now wrong in several places~~ — fixed 2026-08-09

The repo table, the proximity_engine description, and the cross-repo coupling section were all corrected by /parselogs on 2026-08-09: one board with two Kconfig roles, the engine marked as deleted in the port, and a caveat that the PlatformIO symlink rules describe the ESP32 tree only.

D3. Smaller things that will be missed

  • phone_sim's wire tests need a case removed for each deleted characteristic (tests/test_wire.py).
  • beep_vocab must stay shared between the two roles so the buzzer vocabulary can't drift — as a Zephyr module or west project, not a copy.
  • CALIBRATION_V2_DESIGN.md becomes historical. It should say so at the top rather than being deleted; it explains why the problem existed.
  • The capture format (prox_capture.h + tools/capture_ingest.py, kept in sync by make schema) dies with the engine. Check nothing else reads CAP_* records first.

E. Deleted things that something still needed

Fingerprints were the only mechanism that ever noticed an anchor had been moved. Channel sounding measures the distance to the nightstand anchor perfectly honestly even when that anchor is now in the kitchen — so "move the anchor" becomes the cheapest bypass in the system. No tools, no trace, and it survives every other protection.

Nearly free to close, because the anchor is the same board: it has an IMU populated and it's mains-powered, so it can watch for sustained motion and flag itself, and the watch can decline to treat a recently-moved anchor as authoritative until it's re-confirmed in the app.

What needs designing: the motion threshold, what re-confirmation means, and whether clearing the flag is itself a loosening change under §9.1. It probably is — a user who can clear the flag can move an anchor.

E2. The one-second host test suite

The deleted engine's most underrated property was that it compiled and ran on a laptop in about a second, with no hardware. That is worth rebuilding for the v3 §4.4 state machine, the §9 gate, and the schedule recurrence logic — all three are pure logic, all three are where the expensive bugs live, and none of them need an nRF. Keep the corpus tests that describe situations (walk-in, teleport rejection, frozen evidence) and re-point them at the CS backend; drop the ones that test the engine's internals.

E3. The hard override has nowhere to live yet

Chunk J is a ship blocker (BLK-01) gated on a design decision rather than on code. v3 §6.2 reserves the long press and routes all button events through one dispatcher so there's a clean seam — but deliberately does not implement the two-button hold, which isn't buildable on one button and is judged the wrong mechanic anyway. See Safety.md.


F. For the next board spin

Not urgent, and cheap to forget by the time it matters.

  • Antenna placement is the largest item, and it is not a nudge. Both modules sit on the west edge with their antennas 0.6 mm short of it and 1.15 mm from each other. They have to go on opposite edges, and only one of them can afford to overhang. The rule, the arithmetic and one worked arrangement are in Antenna Placement.md; it plausibly sets the outline before anything else does.
  • /POWER_DISABLE (P0.01) is labelled but unrouted — either wire it or drop the label, because a label that goes nowhere looks exactly like one that goes somewhere.
  • A dedicated override button, not the wake button, and not on the back of the case — if Safety.md concludes the override deserves one.
  • Anchor BOM. Every anchor carries a charger, cell, LED ring, IMU and IR sensor it doesn't use. That's the price of one PCB, and one PCB is what makes launch-plan.md §5's FCC scoping honest — so weigh BLK-11 against the certification saving, and note that the "unused" IMU is what makes E1 nearly free.