- 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>
142 lines
7.5 KiB
Markdown
142 lines
7.5 KiB
Markdown
# 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
|
|
|
|
```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, 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`.
|