# MusicMouse — repo overview Orientation doc for AI agents (or humans) working on this repo for the first time. ## What this is MusicMouse is a DIY, Toniebox-style physical music player for kids, shaped like a mouse and living on a shelf. Small 3D-printed animal figurines (fox, owl, dog, elephant, squirrel, crocodile, rabbit, snowman, puppy — see `hardware/3dprints/figures/`) each carry an RFID tag. Placing a figurine on the mouse starts that figure's playlist. The mouse also has a rotary encoder and capacitive touch areas (ears/feet) for volume/skip control, addressable RGBW LED rings with animated effects, and MQTT/Home Assistant integration. ## Repo layout | Path | What it is | |---|---| | `python-backend/` | Python host application — the main runtime. Has its own `README.md` with the architecture; start there for backend work. | | `web/` | React + TypeScript front-end (Vite). Browse the whole library and play any of it from a browser. Built output is served by `python-backend` itself. | | `esp-firmware/` | ESP32 firmware (C++, Arduino framework via PlatformIO). Reads the RFID reader and buttons, drives the LED strips, talks to `python-backend` over serial. | | `hardware/` | 3D-print models for the figurines and enclosure (FreeCAD/Blender/OBJ/STL), a Fritzing electronics sketch, datasheets, and `pinout.md` (RFID reader + button-board wiring). | | `claude-design/` | The interaction and visual spec the web front-end was built from ("Dolphin Beats"), as a standalone HTML mockup with hardcoded data. `web/` is the real implementation; the mockup is kept as the reference for the keyboard model and the styling. It also contains a second page, "Mein Zimmer" (room lights), which is **not** implemented. | ## How the pieces talk to each other - **ESP32 firmware ↔ `python-backend`**: a length-prefixed binary protocol over serial. Frames are `uint32 magic | uint8 type | uint16 size | payload`, little-endian, with firmware log text interleaved on the same link. The Python side lives in `python-backend/musicmouse/devices/wire.py`, the firmware side in `esp-firmware/src/Messages.h`. The contract is hand-duplicated in two languages. `tests/test_wire.py` parses `Messages.h` and fails if the message ids drift, and `tests/test_effects.py` pins the exact bytes of every effect payload — so a firmware change that breaks the host now breaks a test instead of just the LEDs. - **`python-backend` ↔ `web/`**: JSON over HTTP for the library and for commands, plus a push-only websocket at `/api/ws` for state. Commands emit exactly the same *intents* the physical buttons emit, so the web UI has no privileged path — and no way to get out of step with a figure someone puts on the reader. See `python-backend/README.md` for the endpoint list. - **`python-backend` ↔ Home Assistant**: MQTT only. The backend publishes three discoverable lights, a player sensor, a volume number, transport buttons, device triggers for every button and touch area, and a tag scanner for the RFID reader. It does *not* call Home Assistant services directly any more; behaviour like "the left ear turns the room light pink" is an HA automation. See `python-backend/README.md` for the trigger topics and the old colour mapping. ## Running it ```sh python -m musicmouse --config /media/musicmouse/config.yml ``` On a machine with no mouse attached — real audio and a real web UI, no serial port: ```sh python -m musicmouse --config ./config.yml --no-hardware ``` `general.serial_port: simulate` does the same thing from the config, and `general.alsa_device: simulate` swaps in a silent player. Both warn at startup, and both keys are required - omitting one is an error rather than an implicit simulation. Or with no hardware *and* no audio: ```sh python -m musicmouse --config ./config.yml --simulate ``` The simulator runs the whole app against a fake serial link and a fake player, either interactively or from a scenario file. `python-backend/musicmouse.service` is the systemd unit for the device. For front-end work, run the backend (either way above) and then: ```sh cd web && npm install && npm run dev # http://localhost:5173, /api proxied to :8080 ``` `npm run build` writes `web/dist`, which `general.web.static_dir` points at in production so one process serves both the UI and the API. ## Config `python-backend/config.yml.example` documents the schema. In short: - `general.{library.*, serial_port, baudrate, reconnect_interval, alsa_device, min_volume, max_volume, initial_volume, volume_increment, button_leds_brightness, audio_extensions, mqtt.*, web.*}` - `figures..{id, colors}` — `id` is a 5-byte hex RFID tag, unique per figure; `colors` is exactly four (`primary, secondary, bg, accent`), each `"#rrggbb"` or `"wNN"`. `general.library.root` is the one path to the music. The shelves under it are fixed names, not settings, because each has quirks the scanner knows about: ``` /Figuren/
/ one folder per figurine /Musik/ - / albums, grouped by artist /Hörbücher/ - / audiobooks, grouped by character /Kinderpodcasts// shows, newest episode first ``` Config is validated with pydantic: unknown keys are errors, and every problem is reported at once. ## Notes for agents - The backend has `pytest`, `ruff` and `mypy --strict` configured in `python-backend/pyproject.toml`, and no CI. Run all three before proposing changes. - Prefer adding a scenario in `python-backend/scenarios/` over a hand-rolled test when the behaviour is end-to-end — those files are executed by the test suite. - `python-backend/notebooks/` is university course material on chord recognition, not part of the app. - The firmware has no automated tests beyond a PlatformIO `native` env for LED effects. - The front-end has `vitest` over the pure modules (`web/src/lib/`) and `tsc --noEmit`; there is no component-level test harness. `npm run test` and `npx tsc --noEmit` are the two checks. - The library index is cached under `general.library.cache`. It is keyed by file mtime and size, so **a change to how the scanner derives a title, artist or series is invisible until the cache is invalidated** — bump `_INDEX_VERSION` in `musicmouse/library/cache.py` when you touch that logic.