refactor: move modules into core/ sub-package, normalize naming

Phase 1 of package refactor. Moves all source modules into pm3py/core/
with file renames (hf_14a→hf_iso14a, hf_15→hf_iso15, hf_mf→hf_mfc)
and client attribute normalization (hf.a14→hf.iso14a, hf.mf→hf.mfc).
pm3py/__init__.py re-exports from core for backward compat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-18 19:24:05 -07:00
parent ab3d37e7b3
commit 42e6b54f29
21 changed files with 108 additions and 46 deletions

View File

@@ -0,0 +1,52 @@
# 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: Deferred — no sniff infrastructure exists yet**
`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.
Will revisit when sniff infrastructure is implemented.
## Phase 3 — Scaffold `transponders/`
**Status: Not started — models live in sim worktree**
Transponder models exist in `.worktrees/sim-framework/` but haven't been merged to master. Scaffold when ready to migrate.
## Phase 4 — Scaffold `reader/` and `sim/`
**Status: Not started**
Depends on Phase 3 and sim worktree merge.

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:

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,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_15 import HF15Commands from pm3py.core.hf_iso15 import HF15Commands
def test_15_scan(): def test_15_scan():
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_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"")