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)
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
if isinstance(led, int):
led_mask = led & 0x0F
@@ -118,30 +125,41 @@ class HWCommands:
"count": count if action_code in (4, 6) else None,
}
@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,
}
def _parse_led_str(self, led: str) -> int:
"""Parse LED string into bitmask. Color mapping depends on platform.
Physical layout:
RDV4: A=orange, B=green, C=red, D=red2
PM3 Easy: A=green, B=red, C=orange, D=blue
"""
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
for token in led.lower().replace(" ", "").split(","):
if token in LED_MAP:
mask |= LED_MAP[token]
if token in LETTER_MAP:
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:
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
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
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

View File

@@ -51,9 +51,12 @@ def test_status():
def test_led_on_by_name():
transport = make_mock_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"")
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():
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))
payload = transport.send_ng.call_args[0][1]
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 bright == 50