refactor: extract sniff infrastructure into pm3py/sniff/ sub-package

Phase 2 of package refactor. Extracts trace parsing, ISO 15693
decoders, NDEF annotation, and ANSI formatting from core/hf_iso15.py
into dedicated sniff/ modules:
- sniff/trace.py — parse_tracelog (protocol-agnostic)
- sniff/ndef.py — NDEF TLV/record decode
- sniff/decode_iso15.py — 15693 command/response decoders
- sniff/format.py — ANSI color formatting
- sniff/session.py — SniffSession with iso15() method

core/hf_iso15.py re-exports sniff symbols for backward compat.
Also fixes NTAG 5 placement (iso15693, not iso14443a4) in design doc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-18 19:44:54 -07:00
parent ff8c5d4e73
commit 7c43a142e6
10 changed files with 830 additions and 518 deletions

View File

@@ -15,17 +15,38 @@ python -m pytest tests/ -v
pip install -e .
```
## Architecture
## Package structure
- `protocol.py` — Wire constants, CRC-16/A, Cmd enum, PM3Status enum. Source of truth for command IDs and frame formats.
- `transport.py` — Frame encode/decode (`encode_ng_frame`, `encode_mix_frame`, `decode_response_frame`), `PM3Transport` async serial class. USB skips CRC (uses magic `0x3361`), FPC UART uses CRC with byte-swapped wire format (`(low << 8) | high`).
- `client.py``Proxmark3` class (async-first, `Proxmark3.sync()` for REPL), `_SyncProxy`, `FirmwareInfo`, port auto-detection.
- `hw.py` — Hardware commands. LED API is platform-aware (Easy vs RDV4 have different color-to-pin mappings). PWM validation fetches capabilities on first use.
- `lf.py` — LF commands + `T55xxCommands` + `LFSearchResult`. Full tag demod (EM410x, HID, etc.) is NOT implemented — that requires the C client's DSP stack.
- `hf.py` — HF core (tune, search, sniff, dropfield).
- `hf_14a.py` — ISO 14443-A (scan, raw, sniff, sim). Uses MIX frames.
- `hf_15.py` — ISO 15693 (scan, rdbl, wrbl, sniff). Uses NG frames with ISO command payloads.
- `hf_mf.py` — MIFARE Classic (rdbl, wrbl, rdsc, chk, sniff, sim, nested, cident). Read uses NG, write uses MIX.
```
pm3py/
__init__.py # re-exports Proxmark3, PM3Error, PM3Response, Cmd, PM3Status
core/ # wire protocol and device commands
protocol.py # wire constants, CRC-16/A, Cmd enum, PM3Status
transport.py # frame encode/decode, PM3Transport async serial
client.py # Proxmark3 class, _SyncProxy, FirmwareInfo
hw.py # hardware commands, LED API (platform-aware)
hf.py # HF core — tune, search, sniff, dropfield
hf_iso14a.py # ISO 14443-A — scan, raw. Uses MIX frames
hf_iso15.py # ISO 15693 — scan, rdbl, wrbl, sniff + trace decoders
hf_mfc.py # MIFARE Classic — rdbl, wrbl, rdsc, chk, nested, cident
lf.py # LF commands + T55xxCommands + LFSearchResult
sniff/ # sniff sessions, trace parsing, protocol decoders (scaffold)
sim/ # card simulation sessions, table compiler, relay (scaffold)
reader/ # higher-level reader modes (scaffold)
transponders/ # tag/transponder models independent of hardware (scaffold)
```
## Client API
```python
pm3.hw.ping() # hardware
pm3.hf.iso14a.scan() # ISO 14443-A
pm3.hf.iso15.rdbl(4) # ISO 15693
pm3.hf.mfc.rdbl(0) # MIFARE Classic
pm3.lf.t55.readbl(0) # T55xx
pm3.hf.tune() # HF antenna tune
pm3.hf.dropfield() # drop field
```
## Wire protocol essentials
@@ -37,7 +58,7 @@ pip install -e .
## Key patterns
- All command methods are `async`. The sync wrapper in `_SyncProxy` intercepts via `__getattr__` and calls `loop.run_until_complete()`.
- All command methods are `async`. The sync wrapper in `_SyncProxy` (in `core/client.py`) intercepts via `__getattr__` and calls `loop.run_until_complete()`.
- Command classes hold a `self._t` reference to `PM3Transport` (or mock in tests).
- Tests use `AsyncMock` for transport. Set `hw._is_rdv4 = False` to skip capabilities fetch in LED tests.
- `capabilities()` response parsed at known byte/bit offsets from the C struct (version=7 format).
@@ -84,7 +105,7 @@ Firmware maintenance: atomic single-file commits for easy rebase against upstrea
## Adding new commands
1. Find the `CMD_*` constant in `include/pm3_cmd.h` and add to `Cmd` enum in `protocol.py`
1. Find the `CMD_*` constant in `include/pm3_cmd.h` and add to `Cmd` enum in `core/protocol.py`
2. Check if the C client uses `SendCommandNG` (→ `send_ng`) or `SendCommandMIX` (→ `send_mix`)
3. Check the payload struct in `pm3_cmd.h` and use `struct.pack` to build it
4. Parse the response using `struct.unpack_from` on `resp.data`