Full rearchitecture using Claude
- event bus systen - all components are independent - preparation for web frontend
This commit is contained in:
120
python-backend/README.md
Normal file
120
python-backend/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user