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

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 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.pyhf_iso14a.py
  • hf_15.pyhf_iso15.py
  • hf_mf.pyhf_mfc.py
  • Client attributes: hf.a14hf.iso14a, hf.mfhf.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)

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__.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.a14hf.iso14a, hf.mfhf.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.pyhf_iso14a.py, hf_15.pyhf_iso15.py, hf_mf.pyhf_mfc.py
  • Normalize client attributes: hf.a14hf.iso14a, hf.mfhf.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.