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>
7.2 KiB
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
Proxmark3client —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
Proxmark3instance.
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.pyhf_15.py→hf_iso15.pyhf_mf.py→hf_mfc.py- Client attributes:
hf.a14→hf.iso14a,hf.mf→hf.mfc,hf.iso15stays
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)
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)
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)
from pm3py.reader import InventoryMode
inv = InventoryMode(pm3)
tags = await inv.scan_all_15()
Transponder models (independent of hardware)
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__.pykeeps re-exportingProxmark3,PM3Error,PM3Response,Cmd,PM3Status- Existing
from pm3py import Proxmark3continues 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__.pyre-exports fromcore- 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.pyintosniff/ - Core
hf_iso15.pykeeps 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.pyandhf/iso14443a3/nxp/mifare_classic.py— these have existing models in the sim worktree
Phase 4 — Scaffold reader/ and sim/
reader/withhf/,lf/,modes/stubssim/— migrate from worktree
Each phase is one PR, tests green before merge.