Files
musicmouse/docs/REPO_OVERVIEW.md
Martin Bauer a737ccf108 Bring the docs in line with the code
- 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>
2026-10-04 10:51:27 +02:00

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 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: 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.md for 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 from general.ha, so the token never reaches the browser.
  • 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 sections mqtt, web, ha (room page), lirc (IR remote), screen (monitor on/off), tippen (typing game) and clock (school bus countdown).
  • figures.<name>.{id, colors, kind} — id is a 5-byte hex RFID tag, unique per figure; colors is exactly four (primary, secondary, bg, accent), each "#rrggbb" or "wNN"; kind is music (default) or book.
  • 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.sh runs every check the repo has - backend pytest, ruff and mypy --strict, web vitest, tsc and eslint - 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 native env for LED effects.
  • The front-end has vitest over the pure modules (web/src/lib/), tsc and eslint; 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_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.