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>
232 lines
7.2 KiB
Markdown
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.
|