- event bus systen - all components are independent - preparation for web frontend
121 lines
4.9 KiB
Markdown
121 lines
4.9 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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.
|