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:
231
docs/plans/2026-03-18-refactor-design.md
Normal file
231
docs/plans/2026-03-18-refactor-design.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 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.
|
||||
@@ -45,8 +45,8 @@ Created `pm3py/sniff/__init__.py` placeholder. Will extract into sub-modules whe
|
||||
|
||||
Created full directory tree per design:
|
||||
- `transponders/hf/iso14443a3/nxp/` — MIFARE Classic, Ultralight, NTAG
|
||||
- `transponders/hf/iso14443a4/nxp/` — DESFire, NTAG 5
|
||||
- `transponders/hf/iso15693/nxp/` — ICODE SLIX2
|
||||
- `transponders/hf/iso14443a4/nxp/` — DESFire
|
||||
- `transponders/hf/iso15693/nxp/` — ICODE SLIX2, NTAG 5
|
||||
- `transponders/hf/iso15693/st/` — ST25TV
|
||||
- `transponders/lf/atmel/` — T55xx
|
||||
- `transponders/lf/em/` — EM4100, EM4x05
|
||||
|
||||
Reference in New Issue
Block a user