Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/opendisplay/models/led_flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ def __post_init__(self) -> None:
_check_nibble("mode", self.mode)
if not 1 <= self.brightness <= 16:
raise ValueError(f"brightness out of range: {self.brightness} (must be 1-16)")
if self.group_repeats is not None and not 1 <= self.group_repeats <= 255:
raise ValueError(f"group_repeats out of range: {self.group_repeats} (must be 1-255 or None for infinite)")
# Encoded as group_repeats-1; raw 0xFE is the firmware's infinite
# sentinel, so 255 finite repeats would encode to 0xFE and loop forever.
if self.group_repeats is not None and not 1 <= self.group_repeats <= 254:
raise ValueError(f"group_repeats out of range: {self.group_repeats} (must be 1-254 or None for infinite)")
_check_u8("reserved", self.reserved)

@classmethod
Expand Down Expand Up @@ -125,7 +127,10 @@ def from_bytes(cls, data: bytes) -> LedFlashConfig:
mode = data[0] & 0x0F
brightness = ((data[0] >> 4) & 0x0F) + 1
group_raw = data[10]
group_repeats = None if group_raw == 0xFE else (group_raw + 1)
# 0xFE is the firmware's infinite sentinel; 0xFF (which our encoder never
# emits) is a firmware-valid unbounded value β€” treat both as infinite so
# parsing a device's payload never raises. Finite values decode as raw+1.
group_repeats = None if group_raw in (0xFE, 0xFF) else (group_raw + 1)

return cls(
mode=mode,
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/test_models_led_flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,20 @@ def test_led_flash_config_rejects_invalid_values() -> None:

with pytest.raises(ValueError, match="color out of range"):
LedFlashStep(color=256)


def test_group_repeats_255_rejected_to_avoid_infinite_sentinel() -> None:
# 255 would encode to raw 0xFE (the infinite sentinel) and loop forever (M11).
with pytest.raises(ValueError, match="group_repeats out of range"):
LedFlashConfig(group_repeats=255)


def test_group_repeats_254_is_max_finite() -> None:
cfg = LedFlashConfig(group_repeats=254)
assert cfg.to_bytes()[10] == 253 # raw = group_repeats - 1


def test_from_bytes_accepts_raw_0xff_without_raising() -> None:
payload = bytes([0x70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0])
cfg = LedFlashConfig.from_bytes(payload) # must not raise
assert cfg.group_repeats is None