- 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>
4.1 KiB
Trace Formatter Design
Problem
The sim session trace output is a raw print() with no color, no command decoding, no wrapping, and no leading newline (so it sometimes renders on the REPL prompt). The PM3 C client has rich colored, decoded trace output — we want that for our Python sim traces.
Design
New module: pm3py/sim/trace_fmt.py
A TraceFormatter class that owns all rendering logic. _trace_reader in sim_session.py delegates to it instead of calling print() directly.
Modes
| Mode | Tag | Color |
|---|---|---|
| Sim (we are the tag) | [Sim] |
Magenta (\033[35m) |
| Reader (we are the reader) | [Rdr] |
Cyan (\033[36m) |
| Sniff (passive observer) | [Snf] |
Default/white |
Direction colors
| Direction | Color |
|---|---|
| Reader -> Tag (command) | Cyan (\033[36m) |
| Tag -> Reader (response) | Yellow (\033[33m) |
Additional colors
| Element | Color |
|---|---|
| Command annotation | Green (\033[32m) |
| Hex bytes | Dim (\033[2m) |
Colors suppressed when sys.stdout.isatty() is False.
Layout
\n[Sim] Reader -> Tag: 26 01 00 INVENTORY
^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^
mode prefix(colored) hex(dim) annotation(green)
Prefix column = fixed width (e.g., 24 chars). This is the indent for continuation lines.
Hex bytes are space-separated (de ad be ef, not deadbeef).
Wrapping
When hex + annotation fit on one line: annotation appears inline after hex.
When hex overflows terminal width: hex wraps at prefix column, annotation goes on its own line below (also at prefix column).
\n[Sim] Tag -> Reader: 00 de ad be ef 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 fe a1
READ MULTIPLE BLOCK [0x00, 13 blocks] OK
Terminal width handling
- Cache
os.get_terminal_size()on init - Register
signal.SIGWINCHhandler (Unix) to update cached width - Fallback: re-read every 10 lines if signal registration fails
- Default 80 columns if no TTY
ISO 15693 command decode table
Extracted from command byte (byte index 1 of frame):
| Cmd | Name | Extra annotation |
|---|---|---|
| 0x01 | INVENTORY | mask_len if present |
| 0x02 | STAY QUIET | -- |
| 0x20 | READ SINGLE BLOCK | block number |
| 0x21 | WRITE SINGLE BLOCK | block number + data length |
| 0x23 | READ MULTIPLE BLOCK | start block, count |
| 0x26 | RESET TO READY | -- |
| 0x2B | GET SYSTEM INFO | -- |
| 0x2C | GET MULTIPLE BLOCK SECURITY | -- |
| 0xA1 | NXP READ CONFIG | reg index |
| 0xA2 | NXP WRITE CONFIG | reg index, value |
| 0xB2 | NXP GET RANDOM | -- |
| 0xB3 | NXP SET PASSWORD | pwd_id |
Response decoding: flags byte 0x00 = OK, 0x01 = ERROR + error code. Inventory responses show UID. Read responses show data.
ISO 14443-A command decode table
| Byte(s) | Name |
|---|---|
| 0x26 | REQA |
| 0x52 | WUPA |
| 0x50 00 | HLTA |
| 0x93 20 | ANTICOL CL1 |
| 0x93 70 | SELECT CL1 |
| 0x95 20/70 | ANTICOL/SELECT CL2 |
| 0x97 20/70 | ANTICOL/SELECT CL3 |
| 0xE0 | RATS |
| 0x02/0x03 | I-BLOCK |
| 0xA2/0xA3 | R-ACK |
| 0xB2/0xB3 | R-NAK |
| 0xC2/0xF2 | S(DESELECT)/S(WTX) |
Response decoding: ATQA as ATQA xx xx, SAK as SAK xx, ATS as ATS [len].
API
class TraceFormatter:
def __init__(self, mode: str = "sim"):
"""mode: "sim", "reader", or "sniff" """
def format(self, direction: int, payload: bytes) -> str:
"""Returns fully formatted, colored, wrapped string with leading \\n."""
def print(self, direction: int, payload: bytes) -> None:
"""format() + print to stdout."""
Decoder functions: decode_15693(direction, payload) -> str | None and decode_14443a(direction, payload) -> str | None.
Integration
sim_session.py: instantiateTraceFormatter("sim")instart_15693(), callself._formatter.print()in_trace_reader()- 14443-A trace path: decode table built now, wired up when firmware trace path lands
- No changes to
replay.pyorInteractiveReader.dump_trace()
Out of scope
- Replay/recorder trace formatting
- InteractiveReader dump formatting
- Protocol decode beyond 15693 + 14443-A