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>
This commit is contained in:
134
docs/plans/2026-03-17-trace-formatter-design.md
Normal file
134
docs/plans/2026-03-17-trace-formatter-design.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user