feat(pm3py): validate PWM LED capability using device capabilities, raise on non-PWM LEDs

This commit is contained in:
michael
2026-03-16 00:20:47 -07:00
parent e934eee676
commit cc7dfa89de
2 changed files with 83 additions and 11 deletions

View File

@@ -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("<BBBHH", led_mask, action_code, brightness_val, speed, count)
await self._t.send_ng(Cmd.LED_CONTROL, payload)
return {
@@ -123,6 +134,27 @@ class HWCommands:
raise ValueError(f"Unknown LED: {token!r}. Use: {', '.join(LED_MAP.keys())}")
return mask
async def _check_pwm_capable(self, led_mask: int, action: str) -> 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("<I", resp.data, 1)[0]
bigbuf_size = struct.unpack_from("<I", resp.data, 5)[0]
# Bit flags start at byte 9
flags = 0
# Parse bit flags (bytes 9+)
is_rdv4 = False
compiled_with_lf = False
compiled_with_iso14443a = False
compiled_with_iso15693 = False
if len(resp.data) > 12:
is_rdv4 = bool(resp.data[12] & 0x02) # byte 12, bit 1
if len(resp.data) > 9:
flags = struct.unpack_from("<I", resp.data, 9)[0] if len(resp.data) >= 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: