Deploy rearchitected MusicMouse backend + web frontend

The app grew a python-backend/ and a web/ frontend, so pi_musicmouse no
longer matches it. Rework the role:

- Install a pinned uv and build the venv with Python 3.13; Raspbian
  Bookworm's python3 is 3.11 and the backend uses PEP 695 syntax.
- pip install python-backend/ instead of a requirements.txt, and take
  the systemd unit straight from the checkout so it can't drift.
- Build web/ on the control machine (no npm on the Pi) and rsync
  web/dist over. Chain the checkout -> install -> build -> sync -> restart
  steps through handlers so a run with no repo change does nothing.
- Per-host config files (config-<host>.yml); the schema now differs
  between a host with a real mouse and a display-only one.
- Version to deploy is per host (musicmouse_version), no default.

Add pi_kiosk: autologin user running startx with Firefox in kiosk mode
and no window manager, for a Pi with a monitor attached. Enabled by
mediapi_has_monitor, defaulting off.

Turn musicdolphin into a display-only instance: serial and audio
simulated, driven entirely through the kiosk page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 17:58:51 +02:00
parent 2443a75871
commit 9b1d01115b
19 changed files with 337 additions and 33 deletions

View File

@@ -1,4 +1,20 @@
# pi_musicmouse
Deploys the "Musicmouse" application: checks out its git repo, creates a
Python virtualenv, sets up a media directory, and installs its config file.
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`.
- 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.
- Builds the frontend (`web/`) on the *control machine* (delegated to `localhost`, not
the Pi - no Node/npm is installed on the Pi) and syncs `web/dist` over.
- Installs `/media/musicmouse/config.yml` from `files/config-<inventory_hostname>.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.
- 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).

View File

@@ -0,0 +1,15 @@
---
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"
pi_musicmouse_python_version: "3.13"
pi_musicmouse_uv_version: "0.12.10"
# Where to check out the repo on the *control machine* to build the frontend (npm isn't
# installed on the Pi - see README). Keyed by version so different hosts on different
# versions don't clobber each other's build.
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"

View File

@@ -0,0 +1,29 @@
---
# 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.
general:
library:
root: /media/musicmouse
serial_port: "simulate"
alsa_device: "simulate"
web:
host: "0.0.0.0"
port: 8080
static_dir: /opt/musicmouse/web/dist
button_leds_brightness: 0.5
volume_increment: 5
min_volume: 0
max_volume: 100
figures:
# Placeholder - serial_port is "simulate" so no reader ever reports a tag id, but the
# schema requires at least one figure to be configured.
placeholder:
id: "0000000001"
colors: ["#ffffff", "#ffffff", "#000000", "#ffffff"]

View File

@@ -1,17 +1,26 @@
---
general:
library:
root: /media/musicmouse
serial_port: "/dev/ttyUSB0"
alsa_device: softvol_effects
mqtt:
user: musicmouse
password: KNLEFLZF94yA6Zhj141
server: homeassistant
hass_url: https://ha.bauer.tech
hass_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI4N2ExMzM2ZmQ4ZWQ0ZDgzOWZhMjU3NmZjYTg1NWQ1ZiIsImlhdCI6MTcwNDAyOTUyOSwiZXhwIjoyMDE5Mzg5NTI5fQ.vx9L5bTSey98uc8TwodShaEXSMr-hjXugPsNviR_fEw" # noqa: yaml[line-length]
web:
host: "0.0.0.0"
port: 8080
static_dir: /opt/musicmouse/web/dist
# The old hass_url/hass_token keys don't exist in the current schema - HA
# integration now lives under general.ha (url, token, devices, scenes; needs at
# least one device or scene) - see config.yml.example. Add it back once picked.
button_leds_brightness: 0.5
volume_increment: 5
min_volume: 23
max_volume: 70
serial_port: "/dev/ttyUSB0"
figures:
elefant:

View File

@@ -1,11 +0,0 @@
[Unit]
Description=MusicMouse Player
After=network.target
[Service]
Type=simple
Restart=always
ExecStart=/opt/musicmouse_venv/bin/python /opt/musicmouse/espmusicmouse/host_driver/main.py /media/musicmouse/
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,38 @@
---
- name: Reinstall musicmouse backend
ansible.builtin.command:
cmd: "/usr/local/bin/uv pip install --python /opt/musicmouse/.venv/bin/python /opt/musicmouse/python-backend"
changed_when: true
notify: Restart musicmouse
- name: Build frontend
ansible.builtin.command:
cmd: npm ci
chdir: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}/web"
delegate_to: localhost
become: false
changed_when: true
notify: Compile frontend
- name: Compile frontend
ansible.builtin.command:
cmd: npm run build
chdir: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}/web"
delegate_to: localhost
become: false
changed_when: true
notify: Sync frontend
- name: Sync frontend
ansible.posix.synchronize:
src: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}/web/dist/"
dest: /opt/musicmouse/web/dist/
delete: true
notify: Restart musicmouse
- name: Restart musicmouse
ansible.builtin.systemd:
name: musicmouse
state: restarted
enabled: "yes"
daemon_reload: "yes"

View File

@@ -2,54 +2,111 @@
- name: Packages
ansible.builtin.apt:
name:
- python3
- python3-pip
- python3-vlc # also in venv - but this installs vlc + deps
- git
- libvlc5 # runtime lib python-vlc (installed into the venv by uv) binds against
- vlc-plugin-base
- alsa-utils
- samba
cache_valid_time: 7200
- name: Ensure uv install directory exists
ansible.builtin.file:
path: "/opt/uv-{{ pi_musicmouse_uv_version }}"
state: directory
mode: "0755"
- name: Download and extract uv {{ pi_musicmouse_uv_version }}
ansible.builtin.unarchive:
src: "https://github.com/astral-sh/uv/releases/download/{{ pi_musicmouse_uv_version }}/uv-aarch64-unknown-linux-gnu.tar.gz"
dest: "/opt/uv-{{ pi_musicmouse_uv_version }}"
remote_src: true
extra_opts: ["--strip-components=1"]
creates: "/opt/uv-{{ pi_musicmouse_uv_version }}/uv"
- name: Symlink uv/uvx into /usr/local/bin
ansible.builtin.file:
src: "/opt/uv-{{ pi_musicmouse_uv_version }}/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
state: link
force: true
loop:
- uv
- uvx
- name: Checkout Musicmouse repo
ansible.builtin.git:
repo: "ssh://git@git.bauer.tech:2222/martin/musicmouse.git"
dest: "/opt/musicmouse"
version: "release/1.1"
repo: "{{ pi_musicmouse_repo }}"
dest: /opt/musicmouse
version: "{{ musicmouse_version }}"
accept_hostkey: true
- name: Create and update virtual env
ansible.builtin.pip:
requirements: /opt/musicmouse/espmusicmouse/host_driver/requirements.txt
virtualenv: /opt/musicmouse_venv
virtualenv_command: "/usr/bin/python3 -m venv"
force: true
notify: Reinstall musicmouse backend
- 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"
creates: /opt/musicmouse/.venv/bin/python
- name: Create media directory
ansible.builtin.file:
path: /media/musicmouse
state: directory
mode: "0755"
- name: Install config file
ansible.builtin.copy:
src: config.yml
src: "{{ pi_musicmouse_config_file }}"
dest: /media/musicmouse/config.yml
mode: "0644"
- name: Install systemd service file
notify: Restart musicmouse
- name: Install systemd service file (from the checked-out repo, not a copy kept here)
ansible.builtin.copy:
src: musicmouse.service
dest: /etc/systemd/system/
src: /opt/musicmouse/python-backend/musicmouse.service
dest: /etc/systemd/system/musicmouse.service
mode: "0644"
remote_src: true
notify: Restart musicmouse
- name: Add script to autostart and start now
ansible.builtin.systemd:
name: musicmouse
state: restarted
state: started
enabled: "yes"
daemon_reload: "yes"
- name: Ensure local frontend build cache directory exists
ansible.builtin.file:
path: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
state: directory
mode: "0755"
delegate_to: localhost
become: false
- name: Checkout Musicmouse repo for frontend build (control machine)
ansible.builtin.git:
repo: "{{ pi_musicmouse_repo }}"
dest: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
version: "{{ musicmouse_version }}"
accept_hostkey: true
force: true
delegate_to: localhost
become: false
notify: Build frontend
- name: Samba setup
ansible.builtin.copy:
src: smb.conf
dest: /etc/samba/
mode: "0644"
- name: Restart samba
ansible.builtin.systemd:
name: smbd
state: restarted
enabled: "yes"
daemon_reload: "yes"
# manual steps:
# - set samba passwords with smbpasswd
# - copy music into /media/musicmouse (or via samba share)
# - upload host driver?
# - manual patching of hassclient necessary :( loop has to be passed in, to not use running_event_loop