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

135 lines
4.1 KiB
Markdown

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