fix(pm3py): platform-aware LED color mapping (Easy vs RDV4 have different physical layouts)

This commit is contained in:
michael
2026-03-16 00:22:43 -07:00
parent cc7dfa89de
commit a91d862647
2 changed files with 39 additions and 18 deletions

View File

@@ -73,6 +73,13 @@ class HWCommands:
speed: Cycle time in ms (default 500) speed: Cycle time in ms (default 500)
count: Repeat count, 0=infinite (default 5) count: Repeat count, 0=infinite (default 5)
""" """
# Fetch capabilities if not cached (needed for color mapping + PWM validation)
if self._is_rdv4 is None:
try:
await self.capabilities()
except PM3Error:
pass # unknown platform, default to Easy mapping
# Resolve LED mask # Resolve LED mask
if isinstance(led, int): if isinstance(led, int):
led_mask = led & 0x0F led_mask = led & 0x0F
@@ -118,30 +125,41 @@ class HWCommands:
"count": count if action_code in (4, 6) else None, "count": count if action_code in (4, 6) else None,
} }
@staticmethod def _parse_led_str(self, led: str) -> int:
def _parse_led_str(led: str) -> int: """Parse LED string into bitmask. Color mapping depends on platform.
"""Parse LED string into bitmask. Accepts names, letters, or 'all'."""
LED_MAP = { Physical layout:
"a": 0x01, "b": 0x02, "c": 0x04, "d": 0x08, RDV4: A=orange, B=green, C=red, D=red2
"green": 0x02, "orange": 0x01, "red": 0x04, "blue": 0x08, PM3 Easy: A=green, B=red, C=orange, D=blue
"all": 0x0F, """
} rdv4 = self._is_rdv4 # None if unknown, assume Easy
# Letters always map directly
LETTER_MAP = {"a": 0x01, "b": 0x02, "c": 0x04, "d": 0x08, "all": 0x0F}
# Color names differ by platform
COLOR_MAP_RDV4 = {"green": 0x02, "orange": 0x01, "red": 0x04, "red2": 0x08,
"g": 0x02, "o": 0x01, "r": 0x04, "r2": 0x08}
COLOR_MAP_EASY = {"green": 0x01, "orange": 0x04, "red": 0x02, "blue": 0x08,
"g": 0x01, "o": 0x04, "r": 0x02, "b": 0x08}
color_map = COLOR_MAP_RDV4 if rdv4 else COLOR_MAP_EASY
mask = 0 mask = 0
for token in led.lower().replace(" ", "").split(","): for token in led.lower().replace(" ", "").split(","):
if token in LED_MAP: if token in LETTER_MAP:
mask |= LED_MAP[token] mask |= LETTER_MAP[token]
elif token in color_map:
mask |= color_map[token]
elif token == "blue" and rdv4:
raise ValueError("RDV4 has no blue LED")
elif token in ("red2", "r2") and not rdv4:
raise ValueError("PM3 Easy has no red2 LED, use 'blue' instead")
else: else:
raise ValueError(f"Unknown LED: {token!r}. Use: {', '.join(LED_MAP.keys())}") raise ValueError(f"Unknown LED: {token!r}. Use color names or letters a-d")
return mask return mask
async def _check_pwm_capable(self, led_mask: int, action: str) -> None: async def _check_pwm_capable(self, led_mask: int, action: str) -> None:
"""Raise if any LEDs in mask don't support PWM.""" """Raise if any LEDs in mask don't support PWM."""
# Fetch capabilities if not cached
if self._is_rdv4 is None: if self._is_rdv4 is None:
try: return # can't validate, let it through
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 pwm_mask = self._PWM_LEDS_RDV4 if self._is_rdv4 else self._PWM_LEDS_EASY
non_pwm = led_mask & ~pwm_mask non_pwm = led_mask & ~pwm_mask

View File

@@ -51,9 +51,12 @@ def test_status():
def test_led_on_by_name(): def test_led_on_by_name():
transport = make_mock_transport() transport = make_mock_transport()
hw = HWCommands(transport) hw = HWCommands(transport)
hw._is_rdv4 = False # Easy: green=A=0x01
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("green", on=True)) result = asyncio.get_event_loop().run_until_complete(hw.led("green", on=True))
transport.send_ng.assert_called_once() payload = transport.send_ng.call_args[0][1]
led_mask = payload[0]
assert led_mask == 0x01 # green on Easy = A
def test_led_pulse(): def test_led_pulse():
transport = make_mock_transport() transport = make_mock_transport()
@@ -82,7 +85,7 @@ def test_led_brightness():
result = asyncio.get_event_loop().run_until_complete(hw.led("green", brightness=50)) result = asyncio.get_event_loop().run_until_complete(hw.led("green", brightness=50))
payload = transport.send_ng.call_args[0][1] payload = transport.send_ng.call_args[0][1]
led_mask, action, bright, _, _ = struct.unpack("<BBBHH", payload) led_mask, action, bright, _, _ = struct.unpack("<BBBHH", payload)
assert led_mask == 0x02 # green = B assert led_mask == 0x01 # green on Easy = A
assert action == 3 # pwm assert action == 3 # pwm
assert bright == 50 assert bright == 50