Files
pm3py/docs/plans/2026-07-05-14a-live-trace-plan.md
michael 615d4a6115 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>
2026-07-05 12:58:48 -07:00

77 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)`