From 5763df90f83ec035fd49f4de770d5b56e28dbfaf Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sat, 19 Sep 2026 19:04:57 +0200 Subject: [PATCH] Pin each MusicMouse host to a deploy branch, and freeze musicmouse musicmouse_version pointed both hosts at rearchitect-backend, a branch that only ever existed on a laptop - so the previous commit's rework could never actually have run. Point each host at a deploy/ branch on the remote instead. Those branches are the deployment record: what is installed on that box right now, moved with a force-push when you mean to deploy. musicmouse (Kinderzimmer) is the reason this matters. It is in daily use and still runs the pre-rearchitecture backend out of /opt/musicmouse_venv and espmusicmouse/host_driver/main.py. That commit has no python-backend/ pyproject.toml, no python-backend/musicmouse.service and no web/ at all, so this role cannot install it - and a run would have force-checked-out incompatible code over a working device first and failed afterwards. Set mediapi_install_kidsmusic: false there and leave it alone; deploy/musicmouse records the commit that is installed, for rollback. Add a stat + fail right after the checkout so the same mistake stops with an explanation instead of failing three tasks deeper. Also in the role: - Install the typing game's curriculum next to config.yml. A config with a tippen: section refuses to start without it - curriculum_file is validated as must-exist - so musicdolphin's new config needs it present. - Stop clobbering config.yml. The app writes to it at runtime: parent mode patches the volume keys and the remote-control page rewrites the remote: block. A plain copy: undid that on every run. Install-once by default, with -e pi_musicmouse_force_config=true to push a change deliberately. - Fill out config-musicdolphin.yml against the current schema: library root and cache, an absolute static_dir (../web/dist would resolve to /media/web/dist from /media/musicmouse and silently serve nothing), tippen, and the volume range for a screen-driven instance. ha: is left commented out until the Wohnzimmer-oben entity ids are picked - the ones in the dev config are Arbeitszimmer. No mqtt: (it would publish a device of dead entities on a simulate-only host) and no lirc:/remote: (this host runs irserver, a serial daemon, not the lircd this speaks TCP to). The curriculum is a verbatim copy of app content, so exclude it from ansible-lint rather than reformat it away from its source. Co-Authored-By: Claude Opus 5 --- .ansible-lint | 7 + host_vars/musicdolphin.yml | 2 +- host_vars/musicmouse.yml | 16 +- roles/pi_musicmouse/README.md | 60 +- roles/pi_musicmouse/defaults/main.yml | 19 +- .../files/config-musicdolphin.yml | 76 ++- .../pi_musicmouse/files/config-musicmouse.yml | 6 + .../pi_musicmouse/files/tippen-curriculum.yml | 527 ++++++++++++++++++ roles/pi_musicmouse/tasks/main.yml | 33 +- 9 files changed, 726 insertions(+), 20 deletions(-) create mode 100644 .ansible-lint create mode 100644 roles/pi_musicmouse/files/tippen-curriculum.yml diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..1441055 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,7 @@ +--- +exclude_paths: + # A verbatim copy of the app repo's tippen-curriculum.yml. It is content the game + # reads, not ansible YAML, and reformatting it here to satisfy a line-length rule + # would make this copy diverge from the file it is copied from. Its schema is checked + # by the backend at startup, which is a stronger check than anything lint does. + - roles/pi_musicmouse/files/tippen-curriculum.yml diff --git a/host_vars/musicdolphin.yml b/host_vars/musicdolphin.yml index f3b2e52..14aad61 100644 --- a/host_vars/musicdolphin.yml +++ b/host_vars/musicdolphin.yml @@ -15,4 +15,4 @@ mediapi_has_dhtsensor: true # via pi_kiosk instead. See roles/pi_musicmouse/files/config-musicdolphin.yml. mediapi_install_kidsmusic: true mediapi_has_monitor: true -musicmouse_version: rearchitect-backend +musicmouse_version: deploy/musicdolphin diff --git a/host_vars/musicmouse.yml b/host_vars/musicmouse.yml index 4c1226e..4fbd260 100644 --- a/host_vars/musicmouse.yml +++ b/host_vars/musicmouse.yml @@ -9,5 +9,17 @@ main_user: root mediapi_has_hifiberry_amp: true mediapi_install_bluetooth_monitor: true -mediapi_install_kidsmusic: true -musicmouse_version: rearchitect-backend +# The Kinderzimmer device still runs the pre-rearchitecture backend (deploy/musicmouse +# == b2c060f): /opt/musicmouse_venv and espmusicmouse/host_driver/main.py, installed by +# an older version of this role. The current pi_musicmouse only knows the new +# python-backend/ + web/ layout, so it is deliberately off here - a run would +# force-check-out incompatible code over a device the kids use every day, and would +# fail anyway (that commit has no python-backend/pyproject.toml, no +# python-backend/musicmouse.service and no web/ at all). +# +# deploy/musicmouse stays recorded below as the rollback marker for what is actually +# installed. Turning this back on means migrating the device at the same time: move +# /media/musicmouse/
/ into Figuren/ and switch to the new config schema - +# files/config-musicmouse.yml is already written for that. +mediapi_install_kidsmusic: false +musicmouse_version: deploy/musicmouse diff --git a/roles/pi_musicmouse/README.md b/roles/pi_musicmouse/README.md index d33f43b..6adfbfd 100644 --- a/roles/pi_musicmouse/README.md +++ b/roles/pi_musicmouse/README.md @@ -2,8 +2,9 @@ Deploys the MusicMouse backend (`python-backend/`) and its built web frontend. -- Checks out `musicmouse_version` (a tag or branch - set per host in `host_vars`, no - default) from `musicmouse_repo` to `/opt/musicmouse`. +- Checks out `musicmouse_version` from `pi_musicmouse_repo` to `/opt/musicmouse`, then + **fails early** if that checkout has no `python-backend/pyproject.toml` - see + "Which version" below. - Installs a pinned `uv` release and uses it to create `/opt/musicmouse/.venv` with Python `pi_musicmouse_python_version` (the backend needs 3.12+ for PEP 695 syntax; Raspbian Bookworm's apt python3 is only 3.11) and `pip install` the backend into it. @@ -11,10 +12,55 @@ Deploys the MusicMouse backend (`python-backend/`) and its built web frontend. the Pi - no Node/npm is installed on the Pi) and syncs `web/dist` over. - Installs `/media/musicmouse/config.yml` from `files/config-.yml` (one file per host - the schema differs between a host with a real mouse attached and - a display-only one; see those files' own comments) and the systemd unit copied - straight from the checked-out repo's `python-backend/musicmouse.service`, so it can - never drift from what's actually in the app repo. + a display-only one; see those files' own comments), the typing game's curriculum + beside it, and the systemd unit copied straight from the checked-out repo's + `python-backend/musicmouse.service`, so it can never drift from what's actually in the + app repo. - Sets up a Samba share of `/media/musicmouse` for copying music onto the device. -Manual steps after a first deploy: set the Samba password with `smbpasswd`, and copy -music into `/media/musicmouse` (or via the Samba share). +## Which version + +`musicmouse_version` is set per host in `host_vars`, with no default, and points at a +`deploy/` branch on the remote. Those branches are the deployment record: what is +installed on that box right now. Moving one is a force-push, and the next run follows it +(the `git:` task uses `force: true`): + + git push -f origin rearchitect-backend:deploy/musicdolphin + just mediapis musicdolphin + +This role only understands the post-rearchitecture layout - `python-backend/` as an +installable package plus a `web/` frontend. On older commits the host driver lives at +`espmusicmouse/host_driver/main.py`, there is no `pyproject.toml`, the unit file is +under `esp-firmware/`, there is no frontend at all, and the config is a different format +that this role's config files would fail to load. There is deliberately no legacy path: +the role stops at the checkout with an explanation instead. + +`musicmouse` (Kinderzimmer) is on such an older commit and still working, so it has +`mediapi_install_kidsmusic: false` and this role skips it entirely; `deploy/musicmouse` +exists purely as the rollback marker for what is installed there. Migrating it means +moving `/media/musicmouse/
/` into `Figuren/` and switching to the new config +schema - `files/config-musicmouse.yml` is already written for that. + +## config.yml is install-once + +The app writes to its own config at runtime: parent mode (`?parentMode=1`) patches the +volume keys, and the remote-control page rewrites the whole `remote:` block. A plain +`copy:` would throw those away on every ansible run, so the config task is +`force: "{{ pi_musicmouse_force_config }}"`, default `false`. Push a changed config when +you actually mean to: + + just run mediapis.yml musicdolphin -e pi_musicmouse_force_config=true + +The curriculum file is not written by the app, so it is installed unconditionally. A +config with a `tippen:` section will not start without it - `curriculum_file` is +validated as must-exist - so a host whose config has no `tippen:` section should set +`pi_musicmouse_curriculum_file: ""`. + +## Manual steps after a first deploy + +- Set the Samba password with `smbpasswd`. +- Get the music onto the device. The app repo's `scripts/sync-library.sh` is the way to + do it: as well as the library it carries over the cover and track-analysis cache, + which is the part that costs minutes of DSP per track to recompute, and it knows which + parts of that cache survive the move and which must not be copied. The Samba share + works too, and then the device does all that work itself. diff --git a/roles/pi_musicmouse/defaults/main.yml b/roles/pi_musicmouse/defaults/main.yml index 20fb085..1cd1d41 100644 --- a/roles/pi_musicmouse/defaults/main.yml +++ b/roles/pi_musicmouse/defaults/main.yml @@ -1,7 +1,9 @@ --- pi_musicmouse_repo: "ssh://git@git.bauer.tech:2222/martin/musicmouse.git" -# No default on purpose: which tag/branch to run is a per-host decision, set in host_vars. -# musicmouse_version: "rearchitect-backend" +# No default on purpose: which branch to run is a per-host decision, set in host_vars. +# The deploy/ branches on the remote are the intended values - move one with +# `git push -f origin :deploy/musicdolphin` and re-run this role. +# musicmouse_version: "deploy/musicdolphin" pi_musicmouse_python_version: "3.13" pi_musicmouse_uv_version: "0.12.10" @@ -13,3 +15,16 @@ pi_musicmouse_build_cache: "/tmp/musicmouse-build-cache" # Which files/config-*.yml to install as this host's /media/musicmouse/config.yml. pi_musicmouse_config_file: "config-{{ inventory_hostname }}.yml" + +# The app owns config.yml at runtime: parent mode writes the volume keys back to it, and +# the remote-control page rewrites its `remote:` block. So the config is installed once +# and then left alone, or every ansible run would silently undo those edits. Push a +# changed config deliberately: +# +# just run mediapis.yml musicdolphin -e pi_musicmouse_force_config=true +pi_musicmouse_force_config: false + +# The typing game's lesson plan, installed next to config.yml. A config with a `tippen:` +# section will not start without it - curriculum_file is validated as must-exist. Set to +# "" for a host whose config has no tippen section. +pi_musicmouse_curriculum_file: "tippen-curriculum.yml" diff --git a/roles/pi_musicmouse/files/config-musicdolphin.yml b/roles/pi_musicmouse/files/config-musicdolphin.yml index a9b4efd..4301e7e 100644 --- a/roles/pi_musicmouse/files/config-musicdolphin.yml +++ b/roles/pi_musicmouse/files/config-musicdolphin.yml @@ -1,25 +1,89 @@ --- -# musicdolphin: display-only instance. No physical mouse attached (for -# now), so serial and audio are both "simulate" - the web front-end (shown locally in -# the pi_kiosk role's Firefox kiosk window, and reachable on the LAN for a tablet) is a -# complete way to drive the player on its own. `figures` still needs at least one entry -# (the schema requires it) even though nothing will ever scan a tag here. +# musicdolphin: display-only instance. No physical mouse attached, so serial and audio +# are both "simulate" - the web front-end (shown locally in the pi_kiosk Firefox window, +# and reachable on the LAN for a tablet) is a complete way to drive the player on its +# own. `figures` still needs at least one entry (the schema requires it) even though +# nothing will ever scan a tag here. +# +# This Pi does have a HifiBerry amp (mediapi_has_hifiberry_amp, alsa_card_name +# sndrpihifiberry), so the `softvol_effects` slot from pi_alsasetup exists on it. +# Swapping alsa_device to that is the one-line change that gives this instance real +# sound - the amp is currently shared by squeezelite and shairport only. +# +# Unknown keys are rejected rather than ignored, so a typo fails at startup with the +# path to the offending line. Every key is documented in the app repo's +# python-backend/config.yml.example. general: library: + # The four shelves underneath are fixed names, not settings: Figuren/, Musik/, + # Hoerbuecher/ (with real umlauts) and Kinderpodcasts/. root: /media/musicmouse + # Relative, so it resolves against *this file's* directory - + # /media/musicmouse/.musicmouse-cache. Seeded by the app repo's + # scripts/sync-library.sh; safe to delete, at the cost of recomputing analysis. + cache: .musicmouse-cache + # analysis_workers: 2 + # ^ pointless until this role installs the backend's `analysis` extra + # (librosa + numpy, a heavy build on a Pi). Without it build_analyzer() falls + # back to NullAnalyzer and the reactive background stays static. + # Both required rather than optional on purpose: running blind or silent has to be + # asked for, so a config that lost a line fails loudly instead of booting into + # something that looks like it is working. Startup says so every boot. serial_port: "simulate" alsa_device: "simulate" web: host: "0.0.0.0" port: 8080 + # Absolute on purpose. The repo's example uses ../web/dist because its config sits + # in python-backend/; from /media/musicmouse that would resolve to /media/web/dist + # and silently serve nothing. static_dir: /opt/musicmouse/web/dist + # The typing game. Both paths are relative, so both land in /media/musicmouse/. + # curriculum_file must exist at startup and is installed by this role; + # progress_file is created and written by the app itself. + tippen: + curriculum_file: tippen-curriculum.yml + progress_file: tippen-progress.json + + # Home Assistant "Mein Zimmer" page. Left out until the Wohnzimmer-oben entity ids + # are picked - the dev config's entities are Arbeitszimmer, the wrong room for this + # Pi. An empty ha: block is invalid; the section needs at least one device or scene, + # so uncomment it only once it is filled in: + # + # ha: + # url: "https://ha.bauer.tech" + # token: "" + # devices: + # - entity_id: cover.wohnzimmer_oben_rollo + # name: "Rollo" + # - entity_id: light.wohnzimmer_oben_... + # name: "Licht" + # scenes: + # - entity_id: scene.wohnzimmer_oben_... + # name: "Lesen" + + # No mqtt: section. The Home Assistant integration exposes the *mouse's* lights, + # buttons, touch areas and tag scanner, none of which exist on a simulate-only + # instance - it would publish a device of dead entities. + # + # No lirc: section either, and consequently no top-level `remote:` mapping. This host + # uses mediapi_ir_control: irserver, a custom serial daemon on /dev/ttyUSB0; the + # backend's LircConfig speaks lircd's TCP protocol (pi_lirc, port 2222) and cannot + # talk to it. + + # Backlight of the prev/next buttons while a figure is playing, 0..1. Inert here + # (no mouse), kept so the file reads the same as the other hosts'. button_leds_brightness: 0.5 - volume_increment: 5 + + # Volume, 0..100. The full range: this one is driven from a screen, not from a knob a + # child can spin, so there is nothing to protect against. min_volume: 0 max_volume: 100 + initial_volume: 40 + volume_increment: 5 figures: # Placeholder - serial_port is "simulate" so no reader ever reports a tag id, but the diff --git a/roles/pi_musicmouse/files/config-musicmouse.yml b/roles/pi_musicmouse/files/config-musicmouse.yml index b20126b..994fa71 100644 --- a/roles/pi_musicmouse/files/config-musicmouse.yml +++ b/roles/pi_musicmouse/files/config-musicmouse.yml @@ -1,4 +1,10 @@ --- +# NOT IN USE. The Kinderzimmer device is frozen on the pre-rearchitecture backend +# (host_vars/musicmouse.yml sets mediapi_install_kidsmusic: false, and deploy/musicmouse +# records the commit that is installed), so this role never installs this file today. +# It is kept because it is already written against the new schema and is what the +# migration will use - which also means moving /media/musicmouse/
/ into +# Figuren/, since the new scanner expects the figures under that shelf. general: library: root: /media/musicmouse diff --git a/roles/pi_musicmouse/files/tippen-curriculum.yml b/roles/pi_musicmouse/files/tippen-curriculum.yml new file mode 100644 index 0000000..3766ad1 --- /dev/null +++ b/roles/pi_musicmouse/files/tippen-curriculum.yml @@ -0,0 +1,527 @@ +# The typing game's lesson plan, referenced from config.yml's general.tippen.curriculum_file. +# +# Every lesson: title, subtitle (read aloud). Then either: +# - keys: the letters-kind keys this round has on the table. At most two of them may +# be new; the rest are keys already taught, written out to say "these are in play +# too". The loader tracks which keys were already active and spotlights only what is +# new, so `keys: [f, j, a, d, k]` right after `[f, j, a, d]` is a K lesson with the +# other four as review ("isolated" - heavy weight on K). A round that introduces +# nothing new spotlights its whole list instead ("mixed" - lighter weight, blended +# with everything learned so far), so two lessons with the same `keys` back to back +# is exactly how you write "isolated, then mixed" - no separate flag needed. +# - drill: true - a pure review round, no new content, whatever is active so far. +# - kind + words - a fragments/words/sentences consolidation round (dive mode by +# default; `mode:` overrides it - see the backend's `ELIGIBLE_MODES` for which +# modes fit which kind). +# +# Letters rounds always play in bubbles mode - the only arcade game that fits a single +# key at a time. +# +# Letter order follows German letter frequency, adapted to a home-row-first, mirrored +# pace for a six-year-old. +# +# Optional reward key, on any lesson: +# +# unlocks: +# +# A path into the music library, written relative to config.yml's +# general.library.root - "Musik/Klaviersammlung/Lied.mp3", not a path from "/" or "~". +# (An absolute path, or one starting with "~", still resolves, so an old file keeps +# working - but relative is the form to write, since it survives moving the library.) +# Passing this lesson (two stars, or five attempts +# regardless of score) unlocks that track and everything before it in the same album, +# inclusive - so pointing at an album's last track unlocks the whole album, and +# pointing at track 5 of an audiobook unlocks chapters 1 through 5. The web front-end +# shows a still-locked track as a question mark, with a note on which lesson unlocks it. +# +# For a podcast (one album per episode under Kinderpodcasts), the same key unlocks that +# episode and every earlier one of the same show, oldest first - so pointing at the +# third episode ever published unlocks the first three, regardless of upload order. +# +# A path that matches nothing in the library is logged as a warning at startup, not a +# fatal error - a moved or mistyped path only loses that one reward, never the app. +# +# Unlike config.yml, this file is never written back to by the app - only progress is. + +worlds: + - number: 1 + title: "Die Grundstellung" + emoji: "🏝️" + reward: clownfish + lessons: + - title: "F und J" + subtitle: "Die Zeigefinger - die Tasten mit den Punkten" + keys: [f, j] + - title: "Mit A" + subtitle: "Das A kommt dazu" + keys: [f, j, a] + - title: "Ja" + subtitle: "Jaja ;)" + kind: fragments + words: [ja, fa, aja, afa, faja] + - title: "Wörter" + subtitle: "Aja" + kind: fragments + mode: feed + words: [ja, fa, jafa, aja, ajaja] + unlocks: "Musik/Klaviersammlung/Coffin Dance - Astronomía 2K.mp3" + - title: "D" + subtitle: "Das D kommt dazu" + keys: [f, j, a, d] + - title: "Wörter" + subtitle: "ja da" + kind: fragments + words: [da ja] + - title: "Wörter" + subtitle: "ja da" + kind: fragments + mode: feed + words: [da ja ada adda fada aja jaja] + - title: "Wörter" + subtitle: "ja da" + kind: fragments + words: [da ja ada adda fada aja jaja ajaja afa fa dafa jafa jada jajaja] + - title: "K" + subtitle: "Der rechte Mittelfinger" + keys: [f, j, a, d, k] + - title: "Wörter" + subtitle: "kaka" + kind: fragments + words: [ka, ak, kak, akka, kaka, daka, kada, jaka, kaja, fakka, kakka] + - title: "Wörter" + subtitle: "kada jaka" + kind: fragments + mode: feed + words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada, + ka, ak, kak, akka, kaka, daka, kada, jaka, kaja, fakka, + ka, ak, kak, akka, kaka, daka, kada, jaka, kaja, fakka] + - title: "S" + subtitle: "Der linke Ringfinger" + keys: [f, j, a, d, k, s] + - title: "Wörter" + subtitle: "sassa" + kind: fragments + words: [sa, as, sas, assa, sass, das, dasa, sada, kasa, saka, fasa, jassa] + - title: "Wörter" + subtitle: "das sassa" + kind: fragments + mode: feed + words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada, + ka, kak, akka, kaka, daka, kada, jaka, kaja, + sa, as, sas, assa, sass, das, dasa, sada, kasa, saka, fasa, jassa, + sa, as, sas, assa, sass, das, dasa, sada, kasa, saka, fasa, jassa] + # Unlocks track 2 of Klaviersammlung, Coffin Dance already being track 1. + unlocks: "Musik/Klaviersammlung/Shakira - Dai Dai.mp3" + - title: "L" + subtitle: "Der rechte Ringfinger" + keys: [f, j, a, d, k, s, l] + - title: "Wörter" + subtitle: "lalla" + kind: fragments + words: [la, al, als, all, lala, alla, salla, lasa, lass, kalla, falla, jalla] + - title: "Wörter" + subtitle: "lass das" + kind: fragments + mode: feed + words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada, + ka, kak, akka, kaka, daka, kada, jaka, kaja, + sa, as, sas, assa, sass, das, dasa, saka, jassa, + la, al, als, all, lala, alla, salla, lasa, lass, kalla, falla, jalla, + la, al, als, all, lala, alla, salla, lasa, lass, kalla, falla, jalla] + # Unlocks track 3 of Klaviersammlung. + unlocks: "Musik/Klaviersammlung/Shakira - Waka Waka (This Time for Africa) (feat. Freshlyground).mp3" + - title: "Ö" + subtitle: "Der rechte kleine Finger" + keys: [f, j, a, d, k, s, l, ö] + - title: "Wörter" + subtitle: "öl" + kind: fragments + words: [öl, ölö, öla, ölda, ösa, öll, sölö, köla, dölö, jölö, fölö, lösa] + - title: "Wörter" + subtitle: "öl kalk salsa" + kind: fragments + words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada, + ka, kak, akka, kaka, daka, kada, jaka, kaja, + sa, as, sas, assa, sass, das, dasa, saka, jassa, + la, al, als, all, lala, alla, salla, lasa, lass, kalla, + öl, ölö, öla, ölda, ösa, öll, sölö, köla, dölö, jölö, fölö, lösa, + öl, ölö, öla, ölda, ösa, öll, sölö, köla, dölö, jölö, fölö, lösa] + - title: "Übung: die Grundstellung" + subtitle: "Alle acht Finger" + drill: true + - title: "Erste kleine Wörter" + subtitle: "Echte Wörter mit acht Tasten" + kind: fragments + words: [da, ja, das, dass, als, all, fall, falls, lass, saal, kalk, salsa, jass, öl] + - title: "Die Leertaste" + subtitle: "Der Daumen kommt dazu" + keys: [" "] + - title: "Übung: Grundstellung mit Leertaste" + subtitle: "Jetzt mit dem Daumen" + drill: true + - title: "Wörter mit Leertaste" + subtitle: "Kleine Wörter, kleine Sätze" + kind: fragments + mode: feed + words: ["lass das", "das da", "da ja", "fall da", "kalk da", "saal da", "ja lass das", "als da", "öl da"] + # Unlocks track 4 of Klaviersammlung - the reward for finishing die Grundstellung. + unlocks: "Musik/Klaviersammlung/Tones and I - Dance Monkey.mp3" + # to here + + - number: 2 + title: "Nach oben" + emoji: "🌊" + reward: octopus + lessons: + - title: "Das E" + subtitle: "Mittelfinger links nach oben" + keys: [e] + - title: "E üben" + subtitle: "Die neue Taste festigen" + keys: [e] + - title: "Das I" + subtitle: "Mittelfinger rechts nach oben" + keys: [i] + - title: "I üben" + subtitle: "Die neue Taste festigen" + keys: [i] + - title: "Übung: E und I" + subtitle: "Beide Mittelfinger nach oben" + drill: true + - title: "Das R" + subtitle: "Zeigefinger links nach oben" + keys: [r] + - title: "R üben" + subtitle: "Die neue Taste festigen" + keys: [r] + - title: "Das U" + subtitle: "Zeigefinger rechts nach oben" + keys: [u] + - title: "U üben" + subtitle: "Die neue Taste festigen" + keys: [u] + - title: "Übung: R und U" + subtitle: "Beide Zeigefinger nach oben" + drill: true + - title: "Kleine Wörter: E I R U" + subtitle: "Erste echte Wörter mit der oberen Reihe" + kind: fragments + words: [die, sie, elf, eis, esel, see, keks, fiel, lied, rad, reis, eier, rufe, feuer, sauer, lauf] + - title: "Das T" + subtitle: "Zeigefinger links weit nach oben" + keys: [t] + - title: "T üben" + subtitle: "Die neue Taste festigen" + keys: [t] + - title: "Das Z" + subtitle: "Zeigefinger rechts weit nach oben" + keys: [z] + - title: "Z üben" + subtitle: "Die neue Taste festigen" + keys: [z] + - title: "Übung: T und Z" + subtitle: "Weit nach oben greifen" + drill: true + - title: "Das O" + subtitle: "Ringfinger rechts nach oben" + keys: [o] + - title: "O üben" + subtitle: "Die neue Taste festigen" + keys: [o] + - title: "Das W" + subtitle: "Ringfinger links nach oben" + keys: [w] + - title: "W üben" + subtitle: "Die neue Taste festigen" + keys: [w] + - title: "Übung: O und W" + subtitle: "Beide Ringfinger nach oben" + drill: true + - title: "Kleine Wörter: T Z O W" + subtitle: "Noch mehr echte Wörter" + kind: fragments + mode: feed + words: [tier, tafel, kette, leiter, zeit, salz, zelt, katze, rot, tor, los, foto, wo, wald, zwei, wolke] + - title: "Das P" + subtitle: "Kleiner Finger rechts nach oben" + keys: [p] + - title: "P üben" + subtitle: "Die neue Taste festigen" + keys: [p] + - title: "Das Q" + subtitle: "Kleiner Finger links nach oben" + keys: [q] + - title: "Q üben" + subtitle: "Die neue Taste festigen" + keys: [q] + - title: "Das Ü" + subtitle: "Kleiner Finger rechts, ganz außen" + keys: [ü] + - title: "Ü üben" + subtitle: "Die neue Taste festigen" + keys: [ü] + - title: "Übung: P Q Ü" + subtitle: "Die kleinen Finger nach oben" + drill: true + - title: "Übung: die obere Reihe" + subtitle: "Die ganze Reihe zusammen" + kind: words + mode: feed + words: [wolke, zeit, pause, prüfe, qualle, torte, reiter, würfel] + - title: "Übung: Welt 2 komplett" + subtitle: "Alles aus der oberen Reihe" + drill: true + # Unlocks the whole audiobook (its last chapter, inclusive of every chapter + # before it). + unlocks: "Hörbücher/Grüffelo/Der Grüffelo/08 - Der Grüffelo - Outro.mp3" + + - number: 3 + title: "Nach unten" + emoji: "🪸" + reward: seahorse + lessons: + - title: "Das N" + subtitle: "Zeigefinger rechts nach unten" + keys: [n] + - title: "N üben" + subtitle: "Die neue Taste festigen" + keys: [n] + - title: "Das M" + subtitle: "Zeigefinger rechts, neben dem N" + keys: [m] + - title: "M üben" + subtitle: "Die neue Taste festigen" + keys: [m] + - title: "Übung: N und M" + subtitle: "Die neuen Tasten festigen" + drill: true + - title: "Das G" + subtitle: "Zeigefinger links, in der Mitte" + keys: [g] + - title: "G üben" + subtitle: "Die neue Taste festigen" + keys: [g] + - title: "Das H" + subtitle: "Zeigefinger rechts, in der Mitte" + keys: [h] + - title: "H üben" + subtitle: "Die neue Taste festigen" + keys: [h] + - title: "Übung: G und H" + subtitle: "Die Mitte der Grundreihe" + drill: true + - title: "Kleine Wörter: N M G H" + subtitle: "Erste echte Wörter nach unten" + kind: fragments + words: [nase, nein, kind, wind, sonne, mama, mond, meer, maus, gut, gans, regen, hase, haus, hund, hupe] + - title: "Das C" + subtitle: "Mittelfinger links nach unten" + keys: [c] + - title: "C üben" + subtitle: "Die neue Taste festigen" + keys: [c] + - title: "Das V" + subtitle: "Zeigefinger links nach unten" + keys: [v] + - title: "V üben" + subtitle: "Die neue Taste festigen" + keys: [v] + - title: "Übung: C und V" + subtitle: "Nach unten greifen" + drill: true + - title: "Das B" + subtitle: "Zeigefinger links, neben dem V" + keys: [b] + - title: "B üben" + subtitle: "Die neue Taste festigen" + keys: [b] + - title: "Das Y" + subtitle: "Kleiner Finger links nach unten" + keys: [y] + - title: "Y üben" + subtitle: "Die neue Taste festigen" + keys: [y] + - title: "Übung: B und Y" + subtitle: "Ganz unten links" + drill: true + - title: "Kleine Wörter: C V B Y" + subtitle: "Noch mehr echte Wörter" + kind: fragments + words: [koch, milch, schule, chaos, vier, vase, voll, vater, baum, boot, bunt, brot, yoga, baby, typ, pony] + - title: "Das X" + subtitle: "Ringfinger links nach unten" + keys: [x] + - title: "X üben" + subtitle: "Die neue Taste festigen" + keys: [x] + - title: "Das Ä" + subtitle: "Kleiner Finger rechts, ganz außen" + keys: [ä] + - title: "Ä üben" + subtitle: "Die neue Taste festigen" + keys: [ä] + - title: "Übung: X und Ä" + subtitle: "Die letzten beiden Tasten" + drill: true + - title: "Übung: alle Buchstaben" + subtitle: "Das ganze Alphabet" + kind: words + mode: race + words: [delfin, wasser, xylofon, bäume, vogel, qualle, muschel, tauchen] + - title: "Übung: Welt 3 komplett" + subtitle: "Alles aus der unteren Reihe" + drill: true + # Unlocks the first five episodes of GEOlino Spezial, oldest first. + unlocks: "Kinderpodcasts/GEOlino Spezial/20200323 - Das Immunsystem.mp3" + + - number: 4 + title: "Große Buchstaben" + emoji: "👑" + reward: turtle + lessons: + - title: "Umschalttaste rechts, Teil 1" + subtitle: "Große Buchstaben der linken Hand" + keys: ["⇧"] + kind: words + words: [Delfin, Wal, Fisch, Baum] + - title: "Umschalttaste rechts, Teil 2" + subtitle: "Noch mehr große Buchstaben" + kind: words + words: [Garten, Ente, Vogel, Riff] + - title: "Umschalttaste links, Teil 1" + subtitle: "Große Buchstaben der rechten Hand" + kind: words + words: [Haus, Kind, Mond, Nase] + - title: "Umschalttaste links, Teil 2" + subtitle: "Noch mehr große Buchstaben" + kind: words + words: [Lampe, Onkel, Uhr, Puppe] + - title: "Übung: Namen, Teil 1" + subtitle: "Namen fangen groß an" + kind: words + words: [Anna, Lena, Paul, Mia, Emil, Jonas, Tom, Lisa] + - title: "Übung: Namen, Teil 2" + subtitle: "Noch mehr Namen" + kind: words + mode: feed + words: [Ben, Nora, Finn, Ida, Max, Ella, Oskar, Greta] + - title: "Übung: große und kleine" + subtitle: "Beides gemischt" + kind: words + words: ["Das Meer", "Ein Delfin", "Die Sonne", "Mein Boot", "Der Wal", "Eine Muschel", "Ein Fisch", "Das Riff", "Mein Ball", "Die Welle", "Ein Stern", "Der Hai"] + - title: "Übung: Welt 4 komplett" + subtitle: "Groß und klein zusammen" + kind: words + drill: true + words: [Delfin, Haus, Anna, Ben, "Das Meer", "Der Wal", Mond, Riff] + + - number: 5 + title: "Ganze Sätze" + emoji: "📖" + reward: pearlmussel + lessons: + - title: "Der Punkt" + subtitle: "Ringfinger rechts nach unten" + keys: ["."] + - title: "Punkt üben" + subtitle: "Die neue Taste festigen" + keys: ["."] + - title: "Erste Sätze mit Punkt" + subtitle: "Ein Satz, ein Punkt" + kind: sentences + words: + - "Das Meer ist tief." + - "Der Hund bellt." + - "Ich mag Kekse." + - "Die Sonne scheint." + - "Der Wal ist riesig." + - "Wir gehen baden." + - "Mama liest ein Buch." + - "Der Fisch schwimmt." + - "Heute ist es warm." + - "Ich habe einen Ball." + - "Die Welle ist hoch." + - "Papa kocht Suppe." + - title: "Das Komma" + subtitle: "Mittelfinger rechts nach unten" + keys: [","] + - title: "Komma üben" + subtitle: "Die neue Taste festigen" + keys: [","] + - title: "Sätze mit Komma" + subtitle: "Zwei Gedanken, ein Satz" + kind: sentences + mode: race + words: + - "Ich mag Wale, Delfine und Fische." + - "Erst lesen, dann tippen." + - "Es ist warm, also baden wir." + - "Rot, gelb und blau sind Farben." + - "Wenn es regnet, bleiben wir drinnen." + - "Der Delfin springt, taucht und spielt." + - "Morgen, sagt Papa, fahren wir los." + - "Eins, zwei, drei, vier." + - "Oma, Opa und ich gehen schwimmen." + - "Die Sonne scheint, das Meer glitzert." + - "Muscheln, Steine und Sand liegen am Strand." + - title: "Der Bindestrich" + subtitle: "Kleiner Finger rechts, ganz außen" + keys: ["-"] + - title: "Bindestrich üben" + subtitle: "Die neue Taste festigen" + keys: ["-"] + - title: "Sätze mit Bindestrich" + subtitle: "Zwei Wörter, ein Strich" + kind: sentences + words: + - "Wir spielen mit dem Wasser-Ball." + - "Das ist ein Delfin-Baby." + - "Meine Ur-Oma kommt heute." + - "Wir bauen eine Sand-Burg." + - "Der Fisch-Schwarm ist riesig." + - "Ich trage mein T-Shirt." + - "Das Schwimm-Bad ist offen." + - "Die Bade-Hose ist nass." + - "Wir essen ein Eis-Hörnchen." + - "Das Segel-Boot ist blau." + - "Mein Lieblings-Tier ist der Delfin." + - title: "Fragezeichen und Ausrufezeichen" + subtitle: "Mit der Umschalttaste" + keys: ["ß", "1"] + - title: "Fragezeichen und Ausrufezeichen üben" + subtitle: "Die neuen Tasten festigen" + keys: ["ß", "1"] + - title: "Fragen und Rufe" + subtitle: "Wie klingt ein Satz?" + kind: sentences + words: + - "Wo ist der Delfin?" + - "Das war toll!" + - "Wie geht es dir?" + - "Pass auf!" + - "Kommst du mit?" + - "Der Wal ist so groß!" + - "Hast du Hunger?" + - "Hurra, Ferien!" + - "Was schwimmt da?" + - "Schau mal, ein Hai!" + - "Wie tief ist das Meer?" + - "Wir haben es geschafft!" + - title: "Übung: ganze Sätze" + subtitle: "Alles zusammen" + kind: sentences + drill: true + mode: race + words: + - "Der Delfin schwimmt sehr schnell." + - "Wo ist mein Boot?" + - "Ich tippe jetzt mit zehn Fingern!" + - "Das Meer ist blau, tief und kalt." + - "Kannst du das auch?" + - "Wir bauen eine Sand-Burg am Strand." + - "Die Möwe fliegt über das Wasser." + - "Oma, Opa und ich gehen schwimmen." + - "Das ist ja super!" + - "Wie heißt der große Wal?" + - "Im Riff wohnen bunte Fische." + - "Der Krake hat acht Arme." diff --git a/roles/pi_musicmouse/tasks/main.yml b/roles/pi_musicmouse/tasks/main.yml index 91406d7..85f77ca 100644 --- a/roles/pi_musicmouse/tasks/main.yml +++ b/roles/pi_musicmouse/tasks/main.yml @@ -42,6 +42,23 @@ force: true notify: Reinstall musicmouse backend +- name: Check the checkout has the layout this role knows + ansible.builtin.stat: + path: /opt/musicmouse/python-backend/pyproject.toml + register: pi_musicmouse_pyproject + +- name: Fail on a pre-rearchitecture checkout + ansible.builtin.fail: + msg: >- + {{ musicmouse_version }} has no python-backend/pyproject.toml, so it predates the + rearchitecture and this role cannot install it - the venv, the systemd unit and + the web/ build all live somewhere else on those commits. Point + musicmouse_version at a branch that has it (deploy/musicdolphin), or set + mediapi_install_kidsmusic to false for this host and leave whatever is installed + alone. Stopping here rather than failing three tasks deeper with the real reason + buried. + when: not pi_musicmouse_pyproject.stat.exists + - name: Create virtualenv with Python {{ pi_musicmouse_python_version }} ansible.builtin.command: cmd: "/usr/local/bin/uv venv --python {{ pi_musicmouse_python_version }} /opt/musicmouse/.venv" @@ -58,6 +75,17 @@ src: "{{ pi_musicmouse_config_file }}" dest: /media/musicmouse/config.yml mode: "0644" + # Install-once: the app writes this file itself (parent mode's volume keys, the + # remote-control mapping). See pi_musicmouse_force_config in defaults. + force: "{{ pi_musicmouse_force_config }}" + notify: Restart musicmouse + +- name: Install tippen curriculum + ansible.builtin.copy: + src: "{{ pi_musicmouse_curriculum_file }}" + dest: /media/musicmouse/tippen-curriculum.yml + mode: "0644" + when: pi_musicmouse_curriculum_file | length > 0 notify: Restart musicmouse - name: Install systemd service file (from the checked-out repo, not a copy kept here) @@ -108,5 +136,6 @@ daemon_reload: "yes" # manual steps: # - set samba passwords with smbpasswd -# - copy music into /media/musicmouse (or via samba share) -# - manual patching of hassclient necessary :( loop has to be passed in, to not use running_event_loop +# - copy music into /media/musicmouse: scripts/sync-library.sh in the app repo, which +# also carries the cover and analysis cache over (or use the samba share, and let +# the device spend the DSP time itself)