Files
musicmouse/docs/REPO_OVERVIEW.md
Martin Bauer d465a74cde Figures: several albums per figure, picked on the LED ring
A figure folder that holds album folders instead of audio can play any of them,
up to six, in folder order (depth-first, alphabetical). Each is its own album,
titled "Fuchs - <folder>" and of the figure's kind. Putting the figure down plays
the album it played last, or the first, straight away; another figure placed in
between forgets the position in that album, not the album itself.

Meanwhile the ring offers all of them: one section per album from a fixed palette,
edges shifting towards the neighbours' hue, the selected one bright and wobbling.
The knob moves the selection one section per detent and wraps; pressing plays the
selected album, and left alone for 8 s the picker fades into the figure's colours
and the music carries on. While open, the device hands the knob to the picker
(`capture_knob`): turns do not change the volume and the press is not published to
Home Assistant. A tick per step and a confirm "pling" go with it.

The firmware only draws: one new ring effect, LED_WHEEL_EFFECT_SEGMENT_SELECT (23),
re-sent on every detent with the section to glide from. The look is tunable through
a temporary `general.figure_selection` config section until the right values
become constants.

Every UI sound - the screen's blubb, the parents' timer, the picker - now follows
the music volume with a floor, times a new parent-mode "Effekt-Lautstärke"
(`general.effects_volume`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:29:18 +02:00

6.6 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 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

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, mqtt.*, web.*}
  • figures.<name>.{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:

<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

  • 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.
  • Track analysis (librosa) runs in a pool of worker processes - see musicmouse/library/workers.py. Anything an Analyzer returns therefore has to be picklable, and an analyzer that records state in its own instance (a test double counting calls) only behaves as written with analysis_workers=1.