Figures: several albums per figure, picked on the LED ring
A figure folder that holds album folders instead of audio can play any of them, up to six, in folder order (depth-first, alphabetical). Each is its own album, titled "Fuchs - <folder>" and of the figure's kind. Putting the figure down plays the album it played last, or the first, straight away; another figure placed in between forgets the position in that album, not the album itself. Meanwhile the ring offers all of them: one section per album from a fixed palette, edges shifting towards the neighbours' hue, the selected one bright and wobbling. The knob moves the selection one section per detent and wraps; pressing plays the selected album, and left alone for 8 s the picker fades into the figure's colours and the music carries on. While open, the device hands the knob to the picker (`capture_knob`): turns do not change the volume and the press is not published to Home Assistant. A tick per step and a confirm "pling" go with it. The firmware only draws: one new ring effect, LED_WHEEL_EFFECT_SEGMENT_SELECT (23), re-sent on every detent with the section to glide from. The look is tunable through a temporary `general.figure_selection` config section until the right values become constants. Every UI sound - the screen's blubb, the parents' timer, the picker - now follows the music volume with a floor, times a new parent-mode "Effekt-Lautstärke" (`general.effects_volume`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from musicmouse.effects import (
|
||||
OFF,
|
||||
EffectAlexaSwipeConfig,
|
||||
EffectCircularConfig,
|
||||
EffectSegmentSelect,
|
||||
EffectStaticConfig,
|
||||
)
|
||||
from musicmouse.events import (
|
||||
@@ -23,9 +24,12 @@ from musicmouse.events import (
|
||||
Event,
|
||||
LedEffectChanged,
|
||||
RfidTokenRead,
|
||||
RotaryTurned,
|
||||
SelectorPressed,
|
||||
SelectorTurned,
|
||||
TouchButtonPressed,
|
||||
)
|
||||
from musicmouse.hardware import Button, ButtonAction, LedZone, TouchButton
|
||||
from musicmouse.hardware import Button, ButtonAction, LedZone, RotaryDirection, TouchButton
|
||||
from musicmouse.simulator import FakeTransport
|
||||
|
||||
FUCHS = bytes.fromhex("04a1b2c3d4")
|
||||
@@ -82,6 +86,56 @@ async def test_button_press_reaches_the_bus(
|
||||
]
|
||||
|
||||
|
||||
def _turn(increment: int, direction: RotaryDirection) -> bytes:
|
||||
return encode_input_event(RotaryTurned(position=0, increment=increment, direction=direction))
|
||||
|
||||
|
||||
def _knob(action: ButtonAction) -> bytes:
|
||||
return encode_input_event(ButtonEvent(button=Button.ROTARY, action=action))
|
||||
|
||||
|
||||
async def test_a_captured_knob_turns_the_selector_not_the_volume(
|
||||
bus: EventBus, device: MusicMouseDevice, seen: list[Event]
|
||||
) -> None:
|
||||
device.capture_knob(True)
|
||||
device.feed(_turn(2, RotaryDirection.UP))
|
||||
device.feed(_turn(1, RotaryDirection.DOWN))
|
||||
# The first turn after boot reports no increment, but it was still a detent.
|
||||
device.feed(_turn(0, RotaryDirection.UP))
|
||||
await bus.drain()
|
||||
|
||||
assert [e.steps for e in only(seen, SelectorTurned)] == [2, -1, 1]
|
||||
assert not only(seen, RotaryTurned)
|
||||
|
||||
|
||||
async def test_a_captured_knob_press_is_the_selectors_alone(
|
||||
bus: EventBus, device: MusicMouseDevice, seen: list[Event]
|
||||
) -> None:
|
||||
device.capture_knob(True)
|
||||
for action in (ButtonAction.PRESSED, ButtonAction.RELEASED):
|
||||
device.feed(_knob(action))
|
||||
# The other buttons are not captured.
|
||||
device.feed(encode_input_event(ButtonEvent(button=Button.LEFT, action=ButtonAction.PRESSED)))
|
||||
await bus.drain()
|
||||
|
||||
assert len(only(seen, SelectorPressed)) == 1
|
||||
assert [e.button for e in only(seen, ButtonEvent)] == [Button.LEFT]
|
||||
|
||||
|
||||
async def test_a_released_knob_is_a_volume_knob_again(
|
||||
bus: EventBus, device: MusicMouseDevice, seen: list[Event]
|
||||
) -> None:
|
||||
device.capture_knob(True)
|
||||
device.capture_knob(False)
|
||||
device.feed(_turn(1, RotaryDirection.UP))
|
||||
device.feed(_knob(ButtonAction.PRESSED))
|
||||
await bus.drain()
|
||||
|
||||
assert len(only(seen, RotaryTurned)) == 1
|
||||
assert len(only(seen, ButtonEvent)) == 1
|
||||
assert not only(seen, SelectorTurned) and not only(seen, SelectorPressed)
|
||||
|
||||
|
||||
async def test_touch_press_reaches_the_bus(
|
||||
bus: EventBus, device: MusicMouseDevice, seen: list[Event]
|
||||
) -> None:
|
||||
@@ -255,6 +309,19 @@ async def test_an_effect_a_zone_does_not_support_is_reported_not_sent(
|
||||
assert "cannot be sent to the mouse LEDs" in caplog.text
|
||||
|
||||
|
||||
async def test_only_the_ring_can_be_a_selector(
|
||||
bus: EventBus, device: MusicMouseDevice, transport: FakeTransport
|
||||
) -> None:
|
||||
effect = EffectSegmentSelect(colors=(ColorRGBW(1, 0, 0, 0), ColorRGBW(0, 1, 0, 0)))
|
||||
device.set_effect(LedZone.RING, effect)
|
||||
device.set_effect(LedZone.SHELF, effect)
|
||||
await bus.drain()
|
||||
|
||||
sent = transport.effect(LedZone.RING)
|
||||
assert sent is not None and sent.as_bytes() == effect.as_bytes()
|
||||
assert transport.effect(LedZone.SHELF) is None
|
||||
|
||||
|
||||
async def test_button_brightness_sets_both_backlights(
|
||||
device: MusicMouseDevice, transport: FakeTransport
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user