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

4.4 KiB
Raw Permalink Blame History

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)