fix(pm3py): platform-aware LED color mapping (Easy vs RDV4 have different physical layouts)
This commit is contained in:
50
pm3py/hw.py
50
pm3py/hw.py
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user