docs: roadmap index + custom-14a-command plans + backfill plan docs

- Add docs/plans/README.md as the roadmap index cataloguing all design/plan docs.
- Add the custom ISO14443-A command handling design + plan (L3 NTAG I2C
  SECTOR_SELECT / cross-sector reads + native auth; L4 static APDUs + WTX relay),
  produced from a multi-agent design workflow.
- Backfill previously-uncommitted plan docs (trace-formatter, ndef-trace-decode,
  live-sniff, firmware-upstream-rebase, 14a-live-trace) and NTAG5_SECURITY.md.
- Sync CLAUDE.md package structure with the committed transponder models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 12:58:48 -07:00
parent a24f0f803f
commit 615d4a6115
12 changed files with 4673 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
# Live trace for ISO14443-A sim (14a-3, extensible to 14a-4)
Add a live reader↔tag trace to the ISO14443-A card simulation, mirroring the
working ISO15693 sim trace, designed so 14a-4 (ISO-DEP / Layer 4) reuses the
same firmware mechanism. Backed by the feasibility eval (verdict:
**feasible with care** — 86µs FDT does not break it).
## Design invariants (why 86µs is not "too fast")
1. **Response is never touched** — table/precompiled, FPGA-timed via
`EmSendCmd14443aRaw` (SSP-clock alignment + FPGA delay-line). Tracing is
architecturally incapable of affecting FDT.
2. **Both trace pushes deferred to *after* transmit** → zero added work inside
the 86µs window. (A push is a ≤128B memcpy ≈ <200 cycles / <4.2µs anyway.)
3. **Flush ≤1 ring entry per idle iteration** (NOT 15693's bulk-drain) the
only USB-touching part must never overlap a reader-command edge, because
`reply_ng` and the Miller software-UART receive share the ARM thread.
4. **Ring + drop-on-full** lossy under anticollision bursts, but the sim
never stalls. RF timing is preserved; trace is merely lossy in tight gaps.
## P0 — Prerequisite (standalone bug): fix `start_14a` sim-start payload
`sim_session.py:101` builds `payload = atqa + sak + uid` then calls
`send_ng_no_response(HF_ISO14443A_SIMULATE)` with **no payload** (`:123` TODO).
The sim never starts correctly today. Fix: send the proper `HF_ISO14443A_SIMULATE`
NG struct the exact `<BH10sB20sBB16s16sB>` layout already used by the fixed
`hf.iso14a.sim()` (tagtype, flags, uid[10], exitAfter, rats…). Land first;
14a sim is unusable without it, trace or not.
## Firmware (atomic single-file commits + build)
- **F1 `include/pm3_cmd.h`:** add `CMD_HF_ISO14443A_SIM_TRACE` (default id
`0x0339`, next to the 15693 one at `0x0336`); reserve a `FLAG_SIM_TRACE` bit
in the 14a sim `flags` word (default `0x4000`, a free high bit) so the host
opts trace in/out mirrors 15693's `flags & 0x02`.
- **F2+F3 `armsrc/iso14443a.c`:** copy the `TRACE_PUSH`/`TRACE_FLUSH` ring block
from `iso15693.c:2423` but **`ENTRY_MAX` 128** (ISO-DEP I-blocks exceed 64B,
needed for 14a-4) and ring 16; `trace_enabled` from the new flag. Hook into
`SimulateIso14443aTagEx` (`:1725`): buffer received cmd after `:1851` (don't
push); after `EmSendPrecompiledCmd` (`:2554`) do `TRACE_PUSH(0,cmd)` then
`TRACE_PUSH(1,response)`; `TRACE_FLUSH_ONE()` at loop top (`:1844`); full
drain on loop exit.
- **F4:** build `fullimage PLATFORM=PM3GENERIC` must be clean.
## pm3py (reuses the generic scriptable-trace plumbing)
- **Y1 `protocol.py`:** add `Cmd.HF_ISO14443A_SIM_TRACE`.
- **Y2 `sim_session.py:284`:** generalize the trace-reader guard
(`cmd == CMD_HF_ISO15693_SIM_TRACE`) into a `{cmd: (protocol, decoder)}`
dispatch; add the 14a branch (`dir=data[0]&0x0F`, `crc_fail=data[0]&0x80`,
`protocol=1`, `decode_14443a`).
- **Y3 `start_14a`:** synchronous trace path mirroring `start_15693` (raw
pyserial + the existing `_trace_reader` thread) + `trace`/`on_frame`/`quiet`
kwargs. `on_frame`/`quiet`/`entries` + `TraceFormatter(mode='sim')` unchanged.
## I/O model decision (the one real design fork)
- **14a-3:** sync trace path (no WTX relay at Layer 3) avoids dual-reader-on-
one-fd contention.
- **14a-4 (later):** the async `_relay_loop` owns WTX; route `SIM_TRACE` through
*it* (single async reader), not a second thread. Firmware F1F3 are identical
for both layers only the pm3py consumer differs. WTX creates ms-scale idle
windows ideal for flushing.
## Tests
- **T1:** mocked-transport unit test synthetic `CMD_HF_ISO14443A_SIM_TRACE`
frames assert `TraceFormatter`/`on_frame`/`entries` (mirror
`test_trace_scripting.py`).
- **T2 (bench):** phone doing anticollision + SELECT + RATS verify (a) card
never drops, (b) frames stream, tolerating loss during anticollision.
## Risks (+ mitigations)
- Ring fill under heavy Layer-3 polling lossy by design; ring=16, accept loss
in anticollision (uninteresting traffic).
- Flush overlapping a reader edge single-entry-per-idle at loop top only;
never bulk-drain post-transmit. A missed command = reader retry, not teardown.
- `ENTRY_MAX=64` too small for ISO-DEP 128 (RAM 1KB static; check budget).
- pm3py dual reader on one fd 14a-3 sync only; 14a-4 converges to async relay.
## Sequence
`P0 → F1 → F2+F3 → F4 → Y1 → Y2 → Y3 → T1 → (T2 on bench)`