Merge branch 'refactor/core-package' into master

Merges the package refactor (core/ sub-package, normalized naming,
scaffolded sub-packages) while preserving all in-progress sniff
infrastructure. Git rename detection carried WIP changes to new paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-18 19:34:48 -07:00
41 changed files with 150 additions and 47 deletions

View File

@@ -0,0 +1,74 @@
# pm3py Refactor Progress
**Design:** [2026-03-18-refactor-design.md](2026-03-18-refactor-design.md)
**Branch:** `refactor/core-package`
**Started:** 2026-03-18
## Phase 1 — Create `core/`, normalize names, fix imports
**Status: Complete**
### What was done
- Created `pm3py/core/` sub-package
- Moved all source modules into `core/`:
- `protocol.py`, `transport.py`, `client.py`, `hw.py`, `hf.py`, `lf.py` — moved as-is
- `hf_14a.py``hf_iso14a.py` (renamed)
- `hf_15.py``hf_iso15.py` (renamed)
- `hf_mf.py``hf_mfc.py` (renamed)
- Normalized client attributes:
- `hf.a14``hf.iso14a`
- `hf.mf``hf.mfc`
- `hf.iso15` — unchanged
- Created `core/__init__.py` re-exporting: `Proxmark3`, `FirmwareInfo`, `PM3Transport`, `PM3Error`, `PM3Response`, `PM3Status`, `Cmd`
- Updated `pm3py/__init__.py` to import from `.core.*`
- Updated all 10 test files (imports + attribute references)
- **30/30 tests passing**
### Backward compatibility
- `from pm3py import Proxmark3` still works (re-exported)
- **Breaking:** `hf.a14``hf.iso14a`, `hf.mf``hf.mfc`
- **Breaking:** Direct imports like `from pm3py.hf_14a import ...``from pm3py.core.hf_iso14a import ...`
## Phase 2 — Extract `sniff/`
**Status: Scaffolded — extraction deferred**
`hf_iso15.py` is 94 lines of clean reader commands + thin `sniff()` firmware call. The trace parsing, 15693 decoders, NDEF annotation, and ANSI formatting described in the design doc haven't been written yet. Nothing to extract.
Created `pm3py/sniff/__init__.py` placeholder. Will extract into sub-modules when sniff infrastructure is implemented.
## Phase 3 — Scaffold `transponders/`
**Status: Complete (scaffolded)**
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/iso15693/st/` — ST25TV
- `transponders/lf/atmel/` — T55xx
- `transponders/lf/em/` — EM4100, EM4x05
All directories have `__init__.py`. Model migration from sim worktree is a separate effort — the sim framework has ~7.8k LOC with deep inheritance chains (`Transponder``Tag15693``NxpIcodeTag``IcodeSlix2Tag`, etc.) and auth mixins (`NxpPasswordAuth`, `NxpAesAuth`). Base class extraction needs to happen alongside model migration.
## Phase 4 — Scaffold `reader/` and `sim/`
**Status: Complete (scaffolded)**
Created directory trees per design:
- `reader/hf/nxp/` — NXP reader ICs (CLRC663, PN5xx)
- `reader/hf/st/` — ST reader ICs
- `reader/lf/`
- `reader/modes/` — inventory, programming, access control workflows
- `sim/` — sim session, table compiler, relay (migrate from worktree)
All directories have `__init__.py`. Sim migration (~41 files, 7.8k LOC) is a separate effort.
## Next Steps
- Sniff infrastructure: implement trace parsing, 15693 decoders, NDEF annotation, ANSI formatting in `sniff/`
- Transponder migration: extract base classes from sim worktree, populate `transponders/` modules
- Sim migration: move sim session, table compiler, relay into `sim/`
- Reader modes: implement inventory, programming, access workflows in `reader/modes/`

View File

@@ -1,6 +1,6 @@
"""pm3py - Python wire protocol library for Proxmark3.""" """pm3py - Python wire protocol library for Proxmark3."""
from .client import Proxmark3 from .core.client import Proxmark3
from .transport import PM3Error, PM3Response from .core.transport import PM3Error, PM3Response
from .protocol import PM3Status, Cmd from .core.protocol import PM3Status, Cmd
__all__ = ["Proxmark3", "PM3Error", "PM3Response", "PM3Status", "Cmd"] __all__ = ["Proxmark3", "PM3Error", "PM3Response", "PM3Status", "Cmd"]

10
pm3py/core/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""pm3py.core — Wire protocol, transport, and device commands."""
from .client import Proxmark3, FirmwareInfo
from .transport import PM3Transport, PM3Error, PM3Response
from .protocol import PM3Status, Cmd
__all__ = [
"Proxmark3", "FirmwareInfo",
"PM3Transport", "PM3Error", "PM3Response",
"PM3Status", "Cmd",
]

View File

@@ -10,9 +10,9 @@ from .transport import PM3Transport, PM3Response, PM3Error, ProgressCallback
from .hw import HWCommands from .hw import HWCommands
from .lf import LFCommands from .lf import LFCommands
from .hf import HFCommands from .hf import HFCommands
from .hf_14a import HF14ACommands from .hf_iso14a import HF14ACommands
from .hf_15 import HF15Commands from .hf_iso15 import HF15Commands
from .hf_mf import HFMFCommands from .hf_mfc import HFMFCommands
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -68,9 +68,9 @@ class Proxmark3:
self.hw = HWCommands(self._transport) self.hw = HWCommands(self._transport)
self.lf = LFCommands(self._transport) self.lf = LFCommands(self._transport)
self.hf = HFCommands(self._transport) self.hf = HFCommands(self._transport)
self.hf.a14 = HF14ACommands(self._transport) self.hf.iso14a = HF14ACommands(self._transport)
self.hf.iso15 = HF15Commands(self._transport) self.hf.iso15 = HF15Commands(self._transport)
self.hf.mf = HFMFCommands(self._transport) self.hf.mfc = HFMFCommands(self._transport)
self.firmware = FirmwareInfo() self.firmware = FirmwareInfo()
async def connect(self) -> None: async def connect(self) -> None:
@@ -157,7 +157,7 @@ class _SyncProxy:
if isinstance(attr, (HWCommands, LFCommands, HFCommands, if isinstance(attr, (HWCommands, LFCommands, HFCommands,
HF14ACommands, HF15Commands, HFMFCommands)): HF14ACommands, HF15Commands, HFMFCommands)):
return _SyncProxy(attr, self._loop) return _SyncProxy(attr, self._loop)
# Sub-modules attached dynamically (e.g. hf.a14, hf.mf) # Sub-modules attached dynamically (e.g. hf.iso14a, hf.mfc)
if hasattr(attr, '_t'): if hasattr(attr, '_t'):
return _SyncProxy(attr, self._loop) return _SyncProxy(attr, self._loop)
if callable(attr) and inspect.iscoroutinefunction(attr): if callable(attr) and inspect.iscoroutinefunction(attr):

View File

@@ -11,9 +11,9 @@ class HFCommands:
def __init__(self, transport: PM3Transport): def __init__(self, transport: PM3Transport):
self._t = transport self._t = transport
# Sub-modules attached by client.py # Sub-modules attached by client.py
self.a14 = None # type: ignore self.iso14a = None # type: ignore
self.iso15 = None # type: ignore self.iso15 = None # type: ignore
self.mf = None # type: ignore self.mfc = None # type: ignore
async def tune(self, on_progress: ProgressCallback = None) -> dict: async def tune(self, on_progress: ProgressCallback = None) -> dict:
"""Measure HF antenna voltage.""" """Measure HF antenna voltage."""
@@ -38,9 +38,9 @@ class HFCommands:
if asyncio.iscoroutine(ret): if asyncio.iscoroutine(ret):
await ret await ret
if self.a14: if self.iso14a:
try: try:
card = await self.a14.scan() card = await self.iso14a.scan()
if card.get("uid"): if card.get("uid"):
results["iso14443a"] = card results["iso14443a"] = card
except PM3Error: except PM3Error:

1
pm3py/reader/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""pm3py.reader — Higher-level reader modes composing core commands."""

View File

@@ -0,0 +1 @@
"""HF reader modes organized by reader IC manufacturer."""

View File

@@ -0,0 +1 @@
"""NXP reader ICs (CLRC663, PN5xx)."""

View File

@@ -0,0 +1 @@
"""ST Microelectronics reader ICs."""

View File

@@ -0,0 +1 @@
"""LF reader modes."""

View File

@@ -0,0 +1 @@
"""Reader workflow modes — inventory, programming, access control."""

1
pm3py/sim/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""pm3py.sim — Card simulation sessions, table compiler, relay."""

1
pm3py/sniff/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""pm3py.sniff — Sniff sessions, trace parsing, protocol decoders, formatting."""

View File

@@ -0,0 +1 @@
"""pm3py.transponders — Tag/transponder models independent of hardware."""

View File

@@ -0,0 +1 @@
"""HF transponder models organized by ISO standard, then manufacturer."""

View File

@@ -0,0 +1 @@
"""ISO 14443-A Layer 3 transponders (MIFARE Classic, Ultralight, NTAG)."""

View File

@@ -0,0 +1 @@
"""NXP ISO 14443-A Layer 3 transponders."""

View File

@@ -0,0 +1 @@
"""ISO 14443-A Layer 4 (ISO-DEP) transponders (DESFire, NTAG 5 via -4)."""

View File

@@ -0,0 +1 @@
"""NXP ISO 14443-A Layer 4 transponders."""

View File

@@ -0,0 +1 @@
"""ISO 15693 transponder models and NFC Forum Type 5 NDEF."""

View File

@@ -0,0 +1 @@
"""NXP ICODE / NTAG 5 ISO 15693 transponders."""

View File

@@ -0,0 +1 @@
"""ST Microelectronics ISO 15693 transponders."""

View File

@@ -0,0 +1 @@
"""LF transponder models organized by manufacturer."""

View File

@@ -0,0 +1 @@
"""Atmel/Microchip LF transponders (T55xx)."""

View File

@@ -0,0 +1 @@
"""EM Microelectronic LF transponders (EM4100, EM4x05)."""

View File

@@ -1,4 +1,4 @@
from pm3py.protocol import crc16_a from pm3py.core.protocol import crc16_a
def test_crc16_a_empty(): def test_crc16_a_empty():
assert crc16_a(b"") == 0x6363 # init 0xC6C6 reflected = 0x6363 assert crc16_a(b"") == 0x6363 # init 0xC6C6 reflected = 0x6363

View File

@@ -1,8 +1,8 @@
import asyncio import asyncio
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.protocol import Cmd from pm3py.core.protocol import Cmd
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.hf import HFCommands from pm3py.core.hf import HFCommands
def test_hf_tune(): def test_hf_tune():
t = AsyncMock() t = AsyncMock()

View File

@@ -1,9 +1,9 @@
import struct import struct
import asyncio import asyncio
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.protocol import Cmd from pm3py.core.protocol import Cmd
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.hf_14a import HF14ACommands from pm3py.core.hf_iso14a import HF14ACommands
def test_14a_scan(): def test_14a_scan():
t = AsyncMock() t = AsyncMock()

View File

@@ -1,15 +1,14 @@
import asyncio import asyncio
import struct import struct
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.protocol import Cmd from pm3py.core.protocol import Cmd
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.hf_15 import ( from pm3py.core.hf_iso15 import (
HF15Commands, parse_tracelog, TRACELOG_HDR_SIZE, HF15Commands, parse_tracelog, TRACELOG_HDR_SIZE,
decode_15693_request, decode_15693_response, decode_15693, decode_15693_request, decode_15693_response, decode_15693,
format_sniff_line, decode_ndef_annotation, format_sniff_line, decode_ndef_annotation,
) )
def test_15_scan(): def test_15_scan():
t = AsyncMock() t = AsyncMock()
iso15 = HF15Commands(t) iso15 = HF15Commands(t)

View File

@@ -1,9 +1,9 @@
import struct import struct
import asyncio import asyncio
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.protocol import Cmd from pm3py.core.protocol import Cmd
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.hf_mf import HFMFCommands from pm3py.core.hf_mfc import HFMFCommands
def test_mf_rdbl(): def test_mf_rdbl():
t = AsyncMock() t = AsyncMock()

View File

@@ -1,9 +1,9 @@
import struct import struct
import asyncio import asyncio
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.protocol import Cmd from pm3py.core.protocol import Cmd
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.hw import HWCommands from pm3py.core.hw import HWCommands
def make_ng_response(cmd: int, status: int, payload: bytes) -> PM3Response: def make_ng_response(cmd: int, status: int, payload: bytes) -> PM3Response:
return PM3Response(cmd=cmd, status=status, reason=0, ng=True, data=payload) return PM3Response(cmd=cmd, status=status, reason=0, ng=True, data=payload)
@@ -91,7 +91,7 @@ def test_led_brightness():
def test_led_pwm_non_capable_raises(): def test_led_pwm_non_capable_raises():
"""PWM on a non-PWM LED should raise PM3Error.""" """PWM on a non-PWM LED should raise PM3Error."""
from pm3py.transport import PM3Error from pm3py.core.transport import PM3Error
import pytest import pytest
transport = make_mock_transport() transport = make_mock_transport()
hw = HWCommands(transport) hw = HWCommands(transport)

View File

@@ -3,7 +3,7 @@ import struct
import asyncio import asyncio
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from pm3py import Proxmark3, Cmd from pm3py import Proxmark3, Cmd
from pm3py.client import FirmwareInfo from pm3py.core.client import FirmwareInfo
def test_full_api_surface(): def test_full_api_surface():
@@ -14,9 +14,9 @@ def test_full_api_surface():
assert hasattr(pm3, "lf") assert hasattr(pm3, "lf")
assert hasattr(pm3, "hf") assert hasattr(pm3, "hf")
assert hasattr(pm3.lf, "t55") assert hasattr(pm3.lf, "t55")
assert hasattr(pm3.hf, "a14") assert hasattr(pm3.hf, "iso14a")
assert hasattr(pm3.hf, "iso15") assert hasattr(pm3.hf, "iso15")
assert hasattr(pm3.hf, "mf") assert hasattr(pm3.hf, "mfc")
assert hasattr(pm3, "send_ng") assert hasattr(pm3, "send_ng")
assert hasattr(pm3, "send_mix") assert hasattr(pm3, "send_mix")
assert hasattr(pm3, "firmware") assert hasattr(pm3, "firmware")
@@ -27,7 +27,7 @@ def test_hw_ping_roundtrip():
"""Test ping returns structured dict.""" """Test ping returns structured dict."""
pm3 = Proxmark3("/dev/null") pm3 = Proxmark3("/dev/null")
mock_transport = AsyncMock() mock_transport = AsyncMock()
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
ping_data = bytes(range(32)) ping_data = bytes(range(32))
mock_transport.send_ng.return_value = PM3Response( mock_transport.send_ng.return_value = PM3Response(
cmd=Cmd.PING, status=0, reason=0, ng=True, data=ping_data) cmd=Cmd.PING, status=0, reason=0, ng=True, data=ping_data)
@@ -41,21 +41,21 @@ def test_mf_rdbl_returns_hex():
"""Verify MIFARE read returns hex string data.""" """Verify MIFARE read returns hex string data."""
pm3 = Proxmark3("/dev/null") pm3 = Proxmark3("/dev/null")
mock_transport = AsyncMock() mock_transport = AsyncMock()
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
block = bytes(range(16)) block = bytes(range(16))
mock_transport.send_ng.return_value = PM3Response( mock_transport.send_ng.return_value = PM3Response(
cmd=Cmd.HF_MIFARE_READBL, status=0, reason=0, ng=True, data=block) cmd=Cmd.HF_MIFARE_READBL, status=0, reason=0, ng=True, data=block)
pm3.hf.mf._t = mock_transport pm3.hf.mfc._t = mock_transport
result = asyncio.get_event_loop().run_until_complete( result = asyncio.get_event_loop().run_until_complete(
pm3.hf.mf.rdbl(block=4, key="FFFFFFFFFFFF")) pm3.hf.mfc.rdbl(block=4, key="FFFFFFFFFFFF"))
assert result["data"] == "000102030405060708090a0b0c0d0e0f" assert result["data"] == "000102030405060708090a0b0c0d0e0f"
assert result["raw"] == block assert result["raw"] == block
def test_sync_proxy(): def test_sync_proxy():
"""Verify sync proxy wraps async calls.""" """Verify sync proxy wraps async calls."""
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.client import _SyncProxy from pm3py.core.client import _SyncProxy
pm3 = Proxmark3("/dev/null") pm3 = Proxmark3("/dev/null")
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
@@ -83,7 +83,7 @@ def test_firmware_probe_compatible():
pm3._transport = mock_transport pm3._transport = mock_transport
pm3.hw._t = mock_transport pm3.hw._t = mock_transport
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
ping_data = bytes(range(32)) ping_data = bytes(range(32))
vstr = b"Proxmark3 RDV4.0 FW v4.18000\x00" vstr = b"Proxmark3 RDV4.0 FW v4.18000\x00"
@@ -109,7 +109,7 @@ def test_firmware_probe_ping_fails():
pm3._transport = mock_transport pm3._transport = mock_transport
pm3.hw._t = mock_transport pm3.hw._t = mock_transport
from pm3py.transport import PM3Response, PM3Error from pm3py.core.transport import PM3Response, PM3Error
vstr = b"Proxmark3 OLD\x00" vstr = b"Proxmark3 OLD\x00"
version_payload = struct.pack("<III", 0x270B0A40, 256*1024, len(vstr)) + vstr version_payload = struct.pack("<III", 0x270B0A40, 256*1024, len(vstr)) + vstr

View File

@@ -1,9 +1,9 @@
import struct import struct
import asyncio import asyncio
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.protocol import Cmd from pm3py.core.protocol import Cmd
from pm3py.transport import PM3Response from pm3py.core.transport import PM3Response
from pm3py.lf import LFCommands from pm3py.core.lf import LFCommands
def make_response(cmd, status, data): def make_response(cmd, status, data):
return PM3Response(cmd=cmd, status=status, reason=0, ng=True, data=data) return PM3Response(cmd=cmd, status=status, reason=0, ng=True, data=data)

View File

@@ -1,6 +1,6 @@
import struct import struct
from pm3py.transport import encode_ng_frame, decode_response_frame, encode_mix_frame, _crc_to_wire from pm3py.core.transport import encode_ng_frame, decode_response_frame, encode_mix_frame, _crc_to_wire
from pm3py.protocol import Cmd, CMD_PREAMBLE_MAGIC, CMD_POSTAMBLE_NOCRC, RESP_PREAMBLE_MAGIC, RESP_POSTAMBLE_NOCRC, crc16_a from pm3py.core.protocol import Cmd, CMD_PREAMBLE_MAGIC, CMD_POSTAMBLE_NOCRC, RESP_PREAMBLE_MAGIC, RESP_POSTAMBLE_NOCRC, crc16_a
def test_encode_ng_frame_ping_no_payload(): def test_encode_ng_frame_ping_no_payload():
frame = encode_ng_frame(Cmd.PING, b"") frame = encode_ng_frame(Cmd.PING, b"")