- 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>
77 lines
4.4 KiB
Markdown
77 lines
4.4 KiB
Markdown
# 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 F1–F3 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)`
|