From cc7dfa89de29c378a82cdc4dda3d4a6b7054e245 Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 16 Mar 2026 00:20:47 -0700 Subject: [PATCH] feat(pm3py): validate PWM LED capability using device capabilities, raise on non-PWM LEDs --- pm3py/hw.py | 56 ++++++++++++++++++++++++++++++++++++++++++++---- tests/test_hw.py | 38 ++++++++++++++++++++++++++------ 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/pm3py/hw.py b/pm3py/hw.py index 2ca494a..52b41da 100644 --- a/pm3py/hw.py +++ b/pm3py/hw.py @@ -8,8 +8,15 @@ from .transport import PM3Transport, PM3Response, PM3Error, ProgressCallback class HWCommands: """Proxmark3 hardware commands.""" + # PWM-capable LEDs by platform (bitmask) + # PM3 Easy: A (green) and B (red) support PWM + # RDV4: A (orange) and D (red2) support PWM + _PWM_LEDS_EASY = 0x03 # A=0x01 | B=0x02 + _PWM_LEDS_RDV4 = 0x09 # A=0x01 | D=0x08 + def __init__(self, transport: PM3Transport): self._t = transport + self._is_rdv4: bool | None = None # cached after first capabilities() call async def ping(self, length: int = 32) -> dict: """Send ping and verify echo. Returns {success, length, data}.""" @@ -97,6 +104,10 @@ class HWCommands: ACTION_NAMES = {0: "off", 1: "on", 2: "toggle", 3: "pwm", 4: "pulse", 5: "fade", 6: "blink"} + # Validate PWM capability for PWM actions (brightness, pulse, fade) + if action_code in (3, 4, 5): + await self._check_pwm_capable(led_mask, ACTION_NAMES[action_code]) + payload = struct.pack(" None: + """Raise if any LEDs in mask don't support PWM.""" + # Fetch capabilities if not cached + if self._is_rdv4 is None: + try: + await self.capabilities() + except PM3Error: + return # can't validate, let it through + + pwm_mask = self._PWM_LEDS_RDV4 if self._is_rdv4 else self._PWM_LEDS_EASY + non_pwm = led_mask & ~pwm_mask + if non_pwm: + platform = "RDV4" if self._is_rdv4 else "PM3 Easy" + LED_NAMES = {0x01: "A", 0x02: "B", 0x04: "C", 0x08: "D"} + bad = [LED_NAMES[b] for b in (0x01, 0x02, 0x04, 0x08) if non_pwm & b] + good = [LED_NAMES[b] for b in (0x01, 0x02, 0x04, 0x08) if pwm_mask & b] + raise PM3Error( + f"LED(s) {','.join(bad)} do not support {action} on {platform}. " + f"PWM-capable LEDs: {','.join(good)}" + ) + async def dbg(self, level: int | None = None) -> dict: """Get or set debug level. None=get, 0-4=set.""" if level is None: @@ -162,15 +194,31 @@ class HWCommands: version = resp.data[0] baudrate = struct.unpack_from(" 12: + is_rdv4 = bool(resp.data[12] & 0x02) # byte 12, bit 1 if len(resp.data) > 9: - flags = struct.unpack_from("= 13 else resp.data[9] + compiled_with_lf = bool(resp.data[9] & 0x80) # byte 9, bit 7 + if len(resp.data) > 10: + compiled_with_iso14443a = bool(resp.data[10] & 0x40) # byte 10, bit 6 + if len(resp.data) > 11: + compiled_with_iso15693 = bool(resp.data[11] & 0x01) # byte 11, bit 0 + + self._is_rdv4 = is_rdv4 + return { "version": version, "baudrate": baudrate, "bigbuf_size": bigbuf_size, - "raw_flags": flags, + "is_rdv4": is_rdv4, + "compiled_with_lf": compiled_with_lf, + "compiled_with_iso14443a": compiled_with_iso14443a, + "compiled_with_iso15693": compiled_with_iso15693, } async def tune(self, on_progress: ProgressCallback = None) -> dict: diff --git a/tests/test_hw.py b/tests/test_hw.py index e14f571..9a3e7b2 100644 --- a/tests/test_hw.py +++ b/tests/test_hw.py @@ -12,6 +12,15 @@ def make_mock_transport(): transport = AsyncMock() return transport +def _make_caps_response(is_rdv4=False): + """Build a capabilities response with is_rdv4 flag.""" + # version(1) + baudrate(4) + bigbuf_size(4) + 4 bytes of flags + flags = bytearray(4) + flags[0] = 0x80 # compiled_with_lf + if is_rdv4: + flags[3] = 0x02 # is_rdv4 at byte 12 bit 1 + return make_ng_response(Cmd.CAPABILITIES, 0, struct.pack("