Files
pm3py/docs/plans/2026-03-17-trace-formatter-design.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.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.SIGWINCH handler (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: instantiate TraceFormatter("sim") in start_15693(), call self._formatter.print() in _trace_reader()
  • 14443-A trace path: decode table built now, wired up when firmware trace path lands
  • No changes to replay.py or InteractiveReader.dump_trace()

Out of scope

  • Replay/recorder trace formatting
  • InteractiveReader dump formatting
  • Protocol decode beyond 15693 + 14443-A