diff --git a/pm3py/client.py b/pm3py/client.py index 6dc3680..8c16bff 100644 --- a/pm3py/client.py +++ b/pm3py/client.py @@ -1,5 +1,8 @@ """Main Proxmark3 client with nested API.""" -from .transport import PM3Transport, PM3Response, ProgressCallback +import logging +from dataclasses import dataclass, field + +from .transport import PM3Transport, PM3Response, PM3Error, ProgressCallback from .hw import HWCommands from .lf import LFCommands from .hf import HFCommands @@ -7,12 +10,27 @@ from .hf_14a import HF14ACommands from .hf_15 import HF15Commands from .hf_mf import HFMFCommands +log = logging.getLogger(__name__) + + +@dataclass +class FirmwareInfo: + """Firmware version and compatibility information populated on connect.""" + version_string: str = "" + chip_id: str = "" + section_size: int = 0 + ng_protocol: bool = False + compatible: bool = False + warnings: list[str] = field(default_factory=list) + class Proxmark3: """Proxmark3 device client with structured Python API. Usage: async with Proxmark3("/dev/ttyACM0") as pm3: + if not pm3.firmware.compatible: + print("Warnings:", pm3.firmware.warnings) status = await pm3.hw.version() print(status) @@ -28,9 +46,11 @@ class Proxmark3: self.hf.a14 = HF14ACommands(self._transport) self.hf.iso15 = HF15Commands(self._transport) self.hf.mf = HFMFCommands(self._transport) + self.firmware = FirmwareInfo() async def connect(self) -> None: await self._transport.connect() + await self._probe_firmware() async def disconnect(self) -> None: await self._transport.disconnect() @@ -42,6 +62,36 @@ class Proxmark3: async def __aexit__(self, *args) -> None: await self.disconnect() + async def _probe_firmware(self) -> None: + """Ping device and fetch firmware version after connecting.""" + fw = self.firmware + fw.warnings = [] + + # Step 1: Ping to verify NG protocol support + try: + ping_result = await self.hw.ping(32) + fw.ng_protocol = ping_result.get("success", False) + if not fw.ng_protocol: + fw.warnings.append("Ping echo mismatch — device may not support NG protocol") + except PM3Error: + fw.ng_protocol = False + fw.warnings.append("Ping failed — device may not support NG protocol or may be in bootloader mode") + + # Step 2: Fetch firmware version + try: + ver = await self.hw.version() + fw.version_string = ver.get("version_string", "") + fw.chip_id = ver.get("chip_id", "") + fw.section_size = ver.get("section_size", 0) + except PM3Error: + fw.warnings.append("Could not read firmware version — device may be in bootloader mode") + + # Step 3: Assess compatibility + fw.compatible = fw.ng_protocol and len(fw.warnings) == 0 + + for w in fw.warnings: + log.warning("pm3py: %s", w) + async def send_ng(self, cmd: int, payload: bytes = b"", timeout: float = 2.0) -> PM3Response: """Raw NG command -- escape hatch for unwrapped commands.""" diff --git a/tests/test_integration.py b/tests/test_integration.py index 65d4c1c..9537270 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -3,6 +3,7 @@ import struct import asyncio from unittest.mock import AsyncMock, MagicMock from pm3py import Proxmark3, Cmd +from pm3py.client import FirmwareInfo def test_full_api_surface(): @@ -18,6 +19,8 @@ def test_full_api_surface(): assert hasattr(pm3.hf, "mf") assert hasattr(pm3, "send_ng") assert hasattr(pm3, "send_mix") + assert hasattr(pm3, "firmware") + assert isinstance(pm3.firmware, FirmwareInfo) def test_hw_ping_roundtrip(): @@ -47,3 +50,52 @@ def test_mf_rdbl_returns_hex(): pm3.hf.mf.rdbl(block=4, key="FFFFFFFFFFFF")) assert result["data"] == "000102030405060708090a0b0c0d0e0f" assert result["raw"] == block + + +def test_firmware_probe_compatible(): + """Verify firmware probe populates FirmwareInfo on successful connect.""" + pm3 = Proxmark3("/dev/null") + mock_transport = AsyncMock() + pm3._transport = mock_transport + pm3.hw._t = mock_transport + + from pm3py.transport import PM3Response + + ping_data = bytes(range(32)) + vstr = b"Proxmark3 RDV4.0 FW v4.18000\x00" + version_payload = struct.pack(" 0