"""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