- 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>
135 lines
4.1 KiB
Markdown
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
|