- python-backend/README: track analysis is implemented (2e0e6ad); the cache layout, the module table (lirc, screen, timers, podcasts, tippen) and the route table (track analysis, HA proxy, typing game) were missing pieces. - docs/REPO_OVERVIEW: Mein Zimmer and the clock are implemented; Home Assistant is also reached over HTTP through the room page's proxy, not MQTT only; the IR remote, scripts/, the optional config sections and figure kind were not mentioned. - web/README: the key table was the mockup's (Ctrl transport keys, TAB over 'all'); ?pi=1 is now a preset with per-browser overrides, not a read-once flag; the room page and the typing game exist; eslint is part of the checks. - Docstrings that described earlier designs: HaConfig (written as browser-direct in the same commit that built the proxy,a8ed350), services/__init__, events, playback, media, colors, tippen progress and AppHeader, lowPower.ts, and the 'Hoerbuecher' spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.5 KiB
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 specs the web front-end was built from, as standalone HTML mockups with hardcoded data: "Dolphin Beats" (the player), "Mein Zimmer" (the room page) and "Delphin Uhr" (the clock overlay). web/ is the real implementation of all three; the mockups are kept as the reference for the keyboard model and the styling. |
scripts/ |
check.sh (every check, see below) and sync-library.sh (seed a device's library and analysis cache). |
docs/ |
This file. |
How the pieces talk to each other
-
ESP32 firmware ↔
python-backend: a length-prefixed binary protocol over serial. Frames areuint32 magic | uint8 type | uint16 size | payload, little-endian, with firmware log text interleaved on the same link. The Python side lives inpython-backend/musicmouse/devices/wire.py, the firmware side inesp-firmware/src/Messages.h.The contract is hand-duplicated in two languages.
tests/test_wire.pyparsesMessages.hand fails if the message ids drift, andtests/test_effects.pypins 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/wsfor 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. Seepython-backend/README.mdfor the endpoint list. -
python-backend↔ Home Assistant: two separate paths.- MQTT, for the mouse itself. 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. Behaviour like "the left ear
turns the room light pink" is an HA automation, not backend code. See
python-backend/README.mdfor the trigger topics and the old colour mapping. - HTTP, for the web front-end's room page only:
/api/ha/...proxies state reads and service calls to Home Assistant's REST API with the token fromgeneral.ha, so the token never reaches the browser.
- MQTT, for the mouse itself. 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. Behaviour like "the left ear
turns the room light pink" is an HA automation, not backend code. See
-
python-backend↔ IR remote: a TCP client for lircd (general.lirc), turned into the same intents as every other front-end.
Running it
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:
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:
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:
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, podcast_episode_limit}, plus optional sectionsmqtt,web,ha(room page),lirc(IR remote),screen(monitor on/off),tippen(typing game) andclock(school bus countdown).figures.<name>.{id, colors, kind}—idis a 5-byte hex RFID tag, unique per figure;colorsis exactly four (primary, secondary, bg, accent), each"#rrggbb"or"wNN";kindismusic(default) orbook.remote.<digit>— what each IR-remote number key plays. Written by the web UI.
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:
<root>/Figuren/<figure name>/ one folder per figurine (or album subfolders
in it, up to six, picked on the LED ring)
<root>/Musik/<Artist> - <Album>/ albums, grouped by artist
<root>/Hörbücher/<Artist> - <Album>/ audiobooks, grouped by character
<root>/Kinderpodcasts/<Show>/ shows, newest episode first
Config is validated with pydantic: unknown keys are errors, and every problem is reported at once.
Notes for agents
- There is no CI.
scripts/check.shruns every check the repo has - backendpytest,ruffandmypy --strict, webvitest,tscandeslint- and is the gate: run it before proposing changes. A checker that cannot run (mypy once aborted on numpy's stubs for two weeks without anyone noticing) is a failure, not a skip. - 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 LED-effect scratch work, not part of the app.- The firmware has no automated tests beyond a PlatformIO
nativeenv for LED effects. - The front-end has
vitestover the pure modules (web/src/lib/),tscandeslint; there is no component-level test harness. - 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_VERSIONinmusicmouse/library/cache.pywhen you touch that logic. - Track analysis (librosa) runs in a pool of worker processes - see
musicmouse/library/workers.py. Anything anAnalyzerreturns therefore has to be picklable, and an analyzer that records state in its own instance (a test double counting calls) only behaves as written withanalysis_workers=1.