- event bus systen - all components are independent - preparation for web frontend
MusicMouse backend
The host application: it reads RFID tags, buttons and touch areas from the ESP32 firmware over serial, plays music through VLC, drives three LED strips, and exposes everything to Home Assistant over MQTT.
ESP32 ⇄ MusicMouseDevice ─┐ ┌─► MqttService (state out, intents in)
VLC ⇄ VlcPlayer ────────┼──► EventBus ──────────►┤
broker ⇄ MqttService ──────┘ ▲ └─► (a web service would go here)
│
reactions/*.py ── call actions on ──► device / player
Three objects own the outside world, one bus carries everything, and the reactions are the only place that decides what should happen. Adding a new way to control the mouse means adding a service that emits the same intents - no device or reaction changes.
Running it
pip install -e '.[dev]'
python -m musicmouse --config /media/musicmouse/config.yml
See config.yml.example for the schema and musicmouse.service for the systemd unit.
Config problems are reported all at once with the path to each one; unknown keys are
errors, not silent no-ops.
Without hardware
The simulator runs the entire app - real bus, real device, real reactions, real MQTT if configured - against a fake serial link and a fake player.
python -m musicmouse --config ./config.yml --simulate
musicmouse> place fuchs
in RfidTokenRead(04a1b2c3d4, figure='fuchs')
rfid fuchs
led ring: SwipeAndChange(AlexaSwipe(#(1.0, 0.4, 0.0, 0) -> ...))
play playing
musicmouse> press right
play track 1: 01 - Song 1
help lists the verbs. The same verbs go in a scenario file:
python -m musicmouse --config ./config.yml --simulate --script scenarios/smoke.txt
Scenario files run on a virtual clock under pytest, so wait 1s costs microseconds and
every file in scenarios/ is part of the test suite. A session reproduced by hand at
the prompt becomes a regression test by pasting it into a .txt file.
Layout
| Path | What it is |
|---|---|
musicmouse/bus.py |
One FIFO queue on one loop. Thread-safe emit() - which is how libVLC's callback thread stops reaching the serial transport. |
musicmouse/events.py |
The vocabulary: input (something happened), intent (something was requested), state (something changed). |
musicmouse/devices/ |
mouse.py (firmware), player.py (VLC), wire.py (pure codec), serial_link.py (transport + reconnect). |
musicmouse/reactions/ |
The policy. @on(SomeEvent) functions that get the app and act. |
musicmouse/services/mqtt/ |
Home Assistant entities: three lights, a player sensor, a volume number, transport buttons, device triggers, a tag scanner. |
musicmouse/simulator/ |
Fake transport and player, the driver vocabulary, the REPL and the script runner. |
musicmouse/config.py |
Pydantic schema, validation, and human-readable error formatting. |
Checks
pytest # unit + scenario tests
ruff check .
mypy # --strict, configured in pyproject.toml
tests/test_wire.py parses ../esp-firmware/src/Messages.h and fails if the Python
message ids drift from the firmware's - the contract is hand-duplicated in two
languages, and it had already drifted once (BUTTON_EVENT was missing on the Python
side). tests/test_effects.py pins the exact bytes of every effect payload.
LED arbitration
MusicMouseDevice is the single writer to each zone, and the most recent effect wins -
whether it came from a figure animation or from Home Assistant. There is no priority
scheme. Every write emits LedEffectChanged, and the MQTT light entities publish their
state from that rather than echoing their own commands, so HA keeps showing the strip's
real state when a figure animation overrides a colour it set.
Home Assistant
The backend no longer calls Home Assistant directly (hass-client is gone). It
publishes what happened; the automations live in HA.
MQTT device triggers are published for every button (pressed, double_clicked,
long_pressed), every touch area (touched/released), and the RFID reader appears as a
tag scanner. Topics are under musicmouse/trigger/… and musicmouse/tag.
Recreating the old room-light behaviour
Two behaviours used to be hard-coded in main.py and now need automations:
Rotary press toggled the room light. Trigger on the rotary_pressed device
trigger, action light.toggle on light.kinderzimmer_fluter.
Touching a body part set a colour on light.kinderzimmer_fluter and
light.music_mouse_regal_licht:
| Touch area | Old service data |
|---|---|
right_foot |
rgb_color: [235, 255, 67] |
left_foot |
color_temp: 469 |
right_ear |
rgb_color: [101, 49, 255] |
left_ear |
rgb_color: [255, 74, 254] |
Trigger on the corresponding *_touched device trigger and call light.turn_on with
that data.