Full rearchitecture using Claude

- event bus systen
- all components are independent
- preparation for web frontend
This commit is contained in:
2026-08-26 13:22:28 +02:00
parent 55bcc9448c
commit d44c24ec97
70 changed files with 7591 additions and 1011 deletions

View File

@@ -0,0 +1,141 @@
"""Golden-byte tests for the LED effect payloads.
These pin the exact layout the firmware reads back into its C++ structs. If one of
them fails, either the firmware struct changed or an effect field was reordered - both
would otherwise show up only as garbled LEDs on the real device.
"""
from __future__ import annotations
import pytest
from musicmouse.color import ColorHSV, ColorRGBW
from musicmouse.effects import (
EffectAlexaSwipeConfig,
EffectCircularConfig,
EffectRandomTwoColorInterpolationConfig,
EffectReverseSwipe,
EffectStaticConfig,
EffectStaticDetailedConfig,
EffectSwipeAndChange,
LedEffect,
)
def test_color_rgbw_is_four_bytes() -> None:
assert ColorRGBW(1.0, 0.5, 0.0, 0.25).as_bytes().hex() == "ff7f003f"
def test_color_rgbw_rejects_out_of_range_channels() -> None:
with pytest.raises(ValueError, match=r"within 0\.\.1"):
ColorRGBW(1.5, 0, 0, 0).as_bytes()
def test_color_hsv_is_three_floats() -> None:
# h=180.0, s=1.0, v=0.5
assert ColorHSV(180.0, 1.0, 0.5).as_bytes().hex() == "00003443" "0000803f" "0000003f"
def test_color_hsv_from_rgb() -> None:
assert ColorHSV.from_rgb(ColorRGBW(1, 0, 0, 0)) == ColorHSV(0.0, 1.0, 1.0)
assert ColorHSV.from_rgb(ColorRGBW(0, 1, 0, 0)) == ColorHSV(120.0, 1.0, 1.0)
def test_static() -> None:
effect = EffectStaticConfig(ColorRGBW(1.0, 0.5, 0.0, 0.25), begin=3, end=45)
assert effect.as_bytes().hex() == "ff7f003f" "0300" "2d00"
def test_static_detailed() -> None:
effect = EffectStaticDetailedConfig(
ColorRGBW(0, 0, 0, 1.0), increment=4, begin=0.25, end=0.75, transition_time_in_ms=500
)
assert effect.as_bytes().hex() == (
"000000ff" # color
"0400" # increment (uint16)
"0000803e" # begin 0.25f
"0000403f" # end 0.75f
"0000fa43" # transition 500.0f
)
def test_circular() -> None:
effect = EffectCircularConfig(speed=360, width=180, color=ColorRGBW(0, 0, 1, 0))
assert effect.as_bytes().hex() == "0000b443" "00003443" "0000ff00"
def test_reverse_swipe() -> None:
effect = EffectReverseSwipe(swipe_speed=720, bell_curve_width_in_leds=3, start_position=180)
assert effect.as_bytes().hex() == "00003444" "00004040" "00003443"
def test_alexa_swipe() -> None:
effect = EffectAlexaSwipeConfig(
primary_color_width=180,
transition_width=180,
swipe_speed=720,
bell_curve_width_in_leds=3,
start_position=180,
forward=False,
primary_color=ColorRGBW(1, 0, 0, 0),
secondary_color=ColorRGBW(0, 1, 0, 0),
)
assert effect.as_bytes().hex() == (
"00003443" "00003443" "00003444" "00004040" "00003443" # five floats
"00" # forward = false
"ff000000" # primary
"00ff0000" # secondary
)
def test_random_two_color_interpolation() -> None:
effect = EffectRandomTwoColorInterpolationConfig(
cycle_durations_ms=1000,
start_with_existing=True,
num_segments=3,
hue1_random=False,
hue2_random=True,
color1=ColorHSV(180.0, 1.0, 0.5),
color2=ColorHSV(0.0, 0.0, 0.0),
)
assert effect.as_bytes().hex() == (
"e8030000" # cycle_durations_ms int32
"01" # start_with_existing
"03000000" # num_segments int32
"00" # hue1_random
"01" # hue2_random
"000034430000803f0000003f" # color1 hsv
"000000000000000000000000" # color2 hsv
)
def test_rgb_colors_are_converted_to_hsv_on_the_wire() -> None:
"""The firmware struct is HSV; reactions hand it RGBW figure colours."""
as_rgb = EffectRandomTwoColorInterpolationConfig(
color1=ColorRGBW(1, 0, 0, 0), color2=ColorRGBW(0, 1, 0, 0)
)
as_hsv = EffectRandomTwoColorInterpolationConfig(
color1=ColorHSV(0.0, 1.0, 1.0), color2=ColorHSV(120.0, 1.0, 1.0)
)
assert as_rgb.as_bytes() == as_hsv.as_bytes()
def test_swipe_and_change_is_the_two_payloads_concatenated() -> None:
effect = EffectSwipeAndChange()
assert effect.as_bytes() == effect.swipe.as_bytes() + effect.change.as_bytes()
@pytest.mark.parametrize(
("effect", "size"),
[
(EffectStaticConfig(ColorRGBW(0, 0, 0, 0)), 8),
(EffectStaticDetailedConfig(ColorRGBW(0, 0, 0, 0)), 18),
(EffectCircularConfig(), 12),
(EffectAlexaSwipeConfig(), 29),
(EffectRandomTwoColorInterpolationConfig(), 35),
(EffectReverseSwipe(), 12),
(EffectSwipeAndChange(), 64),
],
)
def test_payload_sizes(effect: LedEffect, size: int) -> None:
assert len(effect.as_bytes()) == size