Files
pm3py/docs/plans/2026-03-18-refactor-design.md
michael 7c43a142e6 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>
2026-03-18 19:44:54 -07:00

232 lines
7.2 KiB
Markdown

# pm3py Package Refactor Design
**Date:** 2026-03-18
**Status:** Design complete, ready for implementation
## Motivation
pm3py has grown from a flat wire-protocol library into a full ecosystem with
transponder models, sim sessions, sniff infrastructure, MCU bridges, and AES
crypto. The current flat layout (`pm3py/hf_14a.py`, `pm3py/hf_15.py`, etc.)
doesn't scale — sniff infrastructure is tangled with reader commands, naming is
inconsistent (`hf.a14` vs `hf.iso15`), and there's no clear home for
transponder models or higher-level workflows.
## Design Decisions
### 1. Single package, sub-packages (not namespace packages)
Everything stays under `pm3py/`. One `pip install pm3py`. No separate
installable packages — this is one tool with one hardware target. Split later
if a genuine need arises.
### 2. Hybrid client model
- **Core commands** stay on the `Proxmark3` client — `pm3.hf.iso15.rdbl(4)`.
Feels like the PM3 CLI.
- **Sim and Sniff** are standalone session classes that take the transport (not
the client). They have lifecycle (start/stop/download) that doesn't fit the
stateless command pattern.
- **Reader modes** are higher-level classes that compose core commands. They
take a `Proxmark3` instance.
### 3. Core preserves PM3 CLI feel
Low-level commands (`scan`, `rdbl`, `wrbl`, `sniff`) stay in `core/`. Reader
modes are a layer on top. Someone who just wants `pm3.hf.iso15.rdbl(4)`
shouldn't have to think about "modes."
### 4. Naming follows PM3 conventions
- `hf_14a.py``hf_iso14a.py`
- `hf_15.py``hf_iso15.py`
- `hf_mf.py``hf_mfc.py`
- Client attributes: `hf.a14``hf.iso14a`, `hf.mf``hf.mfc`, `hf.iso15` stays
### 5. Sniff extracted from core
`hf_15.py` is currently ~60% sniff infrastructure (trace parsing, 15693
decoders, NDEF annotation, ANSI formatting) and ~40% reader commands. The
sniff code becomes its own sub-package. Core keeps only thin `.sniff()` firmware
commands.
### 6. Transponder hierarchy: frequency → standard → manufacturer → transponder
For HF, the ISO standard is the second level. For LF, where there's typically
no standard, skip straight to manufacturer.
### 7. Reader hierarchy: frequency → manufacturer
Reader modes organized by frequency, then by reader IC manufacturer.
## Package Structure
```
pm3py/
__init__.py # re-exports from core for backward compat
core/
__init__.py # re-exports Proxmark3, PM3Error, PM3Response, Cmd, PM3Status
protocol.py # wire constants, CRC, Cmd enum, PM3Status
transport.py # frame encode/decode, PM3Transport
client.py # Proxmark3 class, _SyncProxy, FirmwareInfo
hw.py # HWCommands
hf.py # HFCommands — tune, search, dropfield
hf_iso14a.py # HF14ACommands — scan, raw
hf_iso15.py # HF15Commands — scan, rdbl, wrbl (sniff removed)
hf_mfc.py # HFMFCommands — rdbl, wrbl, rdsc, chk, nested, cident
lf.py # LFCommands, T55xxCommands, LFSearchResult
sniff/
__init__.py
session.py # SniffSession — start/stop/download per protocol
trace.py # parse_tracelog() — protocol-agnostic trace buffer parsing
decode_iso15.py # ISO 15693 request/response decoders, command tables
decode_iso14a.py # ISO 14443-A decoder (stub/future)
ndef.py # NDEF TLV/record decode for trace annotation
format.py # ANSI color formatting, format_sniff_line, wrapping
sim/
__init__.py
# sim session, table compiler, relay — moved from worktree
reader/
__init__.py
hf/
__init__.py
nxp/ # NXP reader ICs (CLRC663, PN5xx)
st/ # ST reader ICs
lf/
__init__.py
modes/
__init__.py
inventory.py # multi-tag scan, anti-collision workflows
programming.py # bulk read/write/clone
access.py # Wiegand/OSDP credential operations
transponders/
__init__.py
hf/
__init__.py
iso14443a3/
__init__.py
nxp/
__init__.py
mifare_classic.py
mifare_ultralight.py
ntag.py # NTAG 213/215/216
iso14443a4/
__init__.py
nxp/
__init__.py
desfire.py
iso15693/
__init__.py
common.py # base ISO 15693 system info, block layout
ndef.py # NFC Forum Type 5 NDEF models
nxp/
__init__.py
icode_slix2.py
ntag5.py # NTAG 5 (ISO 15693 / NFC Type 5)
st/
__init__.py
st25tv.py
lf/
__init__.py
atmel/
__init__.py
t55xx.py
em/
__init__.py
em4100.py
em4x05.py
```
## Import Paths
### Core commands (PM3 CLI feel, unchanged)
```python
from pm3py import Proxmark3
async with Proxmark3() as pm3:
await pm3.hw.ping()
await pm3.hf.iso14a.scan()
await pm3.hf.iso15.rdbl(4)
await pm3.hf.mfc.rdbl(0)
await pm3.lf.t55.readbl(0)
await pm3.hf.tune()
await pm3.hf.dropfield()
```
### Sniff and Sim (standalone session classes)
```python
from pm3py.sniff import SniffSession
from pm3py.sim import SimSession
sniff = SniffSession(pm3._transport)
entries = await sniff.iso15(timeout=60.0)
sim = SimSession(pm3._transport)
await sim.start(tag_model)
```
### Reader modes (compose core commands)
```python
from pm3py.reader import InventoryMode
inv = InventoryMode(pm3)
tags = await inv.scan_all_15()
```
### Transponder models (independent of hardware)
```python
from pm3py.transponders.hf.iso14443a3.nxp import MifareClassic1K
from pm3py.transponders.hf.iso15693.nxp import IcodeSlix2
from pm3py.transponders.hf.iso15693 import ndef
```
## Backward Compatibility
- `pm3py/__init__.py` keeps re-exporting `Proxmark3`, `PM3Error`,
`PM3Response`, `Cmd`, `PM3Status`
- Existing `from pm3py import Proxmark3` continues to work
- **Breaking changes:** attribute renames on client only:
`hf.a14``hf.iso14a`, `hf.mf``hf.mfc`
## Migration Phases
### Phase 1 — Create `core/`, normalize names, fix imports
- Create `pm3py/core/` and move protocol, transport, client, hw, hf, lf
- Rename during move: `hf_14a.py``hf_iso14a.py`, `hf_15.py`
`hf_iso15.py`, `hf_mf.py``hf_mfc.py`
- Normalize client attributes: `hf.a14``hf.iso14a`, `hf.mf``hf.mfc`
- `pm3py/__init__.py` re-exports from `core`
- Move and update tests. All green before proceeding.
### Phase 2 — Extract `sniff/`
- Pull trace parsing, 15693 decoders, NDEF annotation, ANSI formatting out of
`hf_iso15.py` into `sniff/`
- Core `hf_iso15.py` keeps only thin `.sniff()` firmware command
- `sniff_decoded()``SniffSession.iso15()`
### Phase 3 — Scaffold `transponders/`
- Create the `hf/iso14443a3/`, `hf/iso14443a4/`, `hf/iso15693/`, `lf/atmel/`,
`lf/em/` tree
- Start with `hf/iso15693/nxp/icode_slix2.py` and
`hf/iso14443a3/nxp/mifare_classic.py` — these have existing models in the
sim worktree
### Phase 4 — Scaffold `reader/` and `sim/`
- `reader/` with `hf/`, `lf/`, `modes/` stubs
- `sim/` — migrate from worktree
Each phase is one PR, tests green before merge.