feat(pm3py): LED API with color names and keyword actions (on/off/pulse/fade/blink/brightness)

This commit is contained in:
michael
2026-03-16 00:13:50 -07:00
parent 29bfccfb41
commit 8cce7eeba3
2 changed files with 101 additions and 9 deletions

View File

@@ -40,16 +40,81 @@ class HWCommands:
resp = await self._t.send_ng(Cmd.STATUS, payload, timeout=2.0 + speed_test_timeout / 1000) resp = await self._t.send_ng(Cmd.STATUS, payload, timeout=2.0 + speed_test_timeout / 1000)
return {"status": resp.status} return {"status": resp.status}
async def led(self, led: int, action: str = "on", brightness: int = 100, async def led(self, led: str | int = "all", *,
speed: int = 500, count: int = 5) -> dict: on: bool = False, off: bool = False, toggle: bool = False,
"""Control LEDs. led: bitmask (1=A,2=B,4=C,8=D). action: on/off/toggle/pwm/pulse/fade/blink.""" brightness: int = -1, pulse: bool = False, fade: bool = False,
actions = {"off": 0, "on": 1, "toggle": 2, "pwm": 3, "pulse": 4, "fade": 5, "blink": 6} blink: bool = False, speed: int = 500, count: int = 5) -> dict:
if action not in actions: """Control LEDs on the Proxmark3.
raise ValueError(f"Unknown action: {action}. Use one of: {list(actions.keys())}")
payload = struct.pack("<BBBHH", led, actions[action], brightness, speed, count) Args:
led: Color name(s), letter(s), or bitmask int.
Names: "green", "red", "orange", "blue", "all"
Letters: "a", "b", "c", "d"
Comma-separated: "green,red"
Bitmask: 1=A, 2=B, 4=C, 8=D
Action kwargs (pick one):
on: Turn LED on
off: Turn LED off
toggle: Toggle LED state
brightness: Set PWM brightness 0-100 (PWM LEDs only)
pulse: Pulse effect (PWM LEDs only)
fade: Fade effect (PWM LEDs only)
blink: Blink effect (all LEDs)
Effect params:
speed: Cycle time in ms (default 500)
count: Repeat count, 0=infinite (default 5)
"""
# Resolve LED mask
if isinstance(led, int):
led_mask = led & 0x0F
else:
led_mask = self._parse_led_str(led)
# Resolve action
if brightness >= 0:
action_code = 3
brightness_val = min(max(brightness, 0), 100)
elif off:
action_code = 0
brightness_val = 0
elif toggle:
action_code = 2
brightness_val = 0
elif pulse:
action_code = 4
brightness_val = 0
elif fade:
action_code = 5
brightness_val = 0
elif blink:
action_code = 6
brightness_val = 0
elif on or True: # default to "on"
action_code = 1
brightness_val = 0
payload = struct.pack("<BBBHH", led_mask, action_code, brightness_val, speed, count)
resp = await self._t.send_ng(Cmd.LED_CONTROL, payload) resp = await self._t.send_ng(Cmd.LED_CONTROL, payload)
return {"status": resp.status} return {"status": resp.status}
@staticmethod
def _parse_led_str(led: str) -> int:
"""Parse LED string into bitmask. Accepts names, letters, or 'all'."""
LED_MAP = {
"a": 0x01, "b": 0x02, "c": 0x04, "d": 0x08,
"green": 0x02, "orange": 0x01, "red": 0x04, "blue": 0x08,
"all": 0x0F,
}
mask = 0
for token in led.lower().replace(" ", "").split(","):
if token in LED_MAP:
mask |= LED_MAP[token]
else:
raise ValueError(f"Unknown LED: {token!r}. Use: {', '.join(LED_MAP.keys())}")
return mask
async def dbg(self, level: int | None = None) -> dict: async def dbg(self, level: int | None = None) -> dict:
"""Get or set debug level. None=get, 0-4=set.""" """Get or set debug level. None=get, 0-4=set."""
if level is None: if level is None:

View File

@@ -39,9 +39,36 @@ def test_status():
result = asyncio.get_event_loop().run_until_complete(hw.status()) result = asyncio.get_event_loop().run_until_complete(hw.status())
assert result["status"] == 0 assert result["status"] == 0
def test_led(): def test_led_on_by_name():
transport = make_mock_transport() transport = make_mock_transport()
hw = HWCommands(transport) hw = HWCommands(transport)
transport.send_ng.return_value = make_ng_response(Cmd.LED_CONTROL, 0, b"") transport.send_ng.return_value = make_ng_response(Cmd.LED_CONTROL, 0, b"")
result = asyncio.get_event_loop().run_until_complete(hw.led(1, "on")) result = asyncio.get_event_loop().run_until_complete(hw.led("green", on=True))
transport.send_ng.assert_called_once() transport.send_ng.assert_called_once()
def test_led_pulse():
transport = make_mock_transport()
hw = HWCommands(transport)
transport.send_ng.return_value = make_ng_response(Cmd.LED_CONTROL, 0, b"")
result = asyncio.get_event_loop().run_until_complete(
hw.led("red", pulse=True, speed=300, count=3))
transport.send_ng.assert_called_once()
# Verify the payload
call_args = transport.send_ng.call_args
payload = call_args[0][1] # second positional arg
led_mask, action, brightness, speed, count = struct.unpack("<BBBHH", payload)
assert led_mask == 0x04 # red = C
assert action == 4 # pulse
assert speed == 300
assert count == 3
def test_led_brightness():
transport = make_mock_transport()
hw = HWCommands(transport)
transport.send_ng.return_value = make_ng_response(Cmd.LED_CONTROL, 0, b"")
result = asyncio.get_event_loop().run_until_complete(hw.led("all", brightness=50))
payload = transport.send_ng.call_args[0][1]
led_mask, action, bright, _, _ = struct.unpack("<BBBHH", payload)
assert led_mask == 0x0F # all
assert action == 3 # pwm
assert bright == 50