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:
@@ -5,3 +5,4 @@ mediapi_has_dhtsensor: false
|
||||
mediapi_disable_onboard_bluetooth: false
|
||||
mediapi_install_bluetooth_monitor: false
|
||||
mediapi_install_kidsmusic: false
|
||||
mediapi_has_monitor: false
|
||||
|
||||
@@ -10,3 +10,9 @@ main_user: root
|
||||
mediapi_has_hifiberry_amp: true
|
||||
mediapi_ir_control: irserver
|
||||
mediapi_has_dhtsensor: true
|
||||
|
||||
# Display-only MusicMouse instance: no physical mouse attached, shown on the monitor
|
||||
# via pi_kiosk instead. See roles/pi_musicmouse/files/config-musicdolphin.yml.
|
||||
mediapi_install_kidsmusic: true
|
||||
mediapi_has_monitor: true
|
||||
musicmouse_version: rearchitect-backend
|
||||
|
||||
@@ -10,3 +10,4 @@ main_user: root
|
||||
mediapi_has_hifiberry_amp: true
|
||||
mediapi_install_bluetooth_monitor: true
|
||||
mediapi_install_kidsmusic: true
|
||||
musicmouse_version: rearchitect-backend
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
when: mediapi_has_hifiberry_amp
|
||||
- role: pi_musicmouse
|
||||
when: mediapi_install_kidsmusic
|
||||
- role: pi_kiosk
|
||||
when: mediapi_has_monitor
|
||||
- pi_squeezelite_custom
|
||||
- pi_shairport
|
||||
- role: pi_irserver
|
||||
|
||||
18
roles/pi_kiosk/README.md
Normal file
18
roles/pi_kiosk/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# pi_kiosk
|
||||
|
||||
Turns a Pi with a monitor attached into a full-screen Firefox kiosk display, with no
|
||||
window manager: an autologin console user runs `startx`, whose `.xinitrc` launches
|
||||
Firefox in `--kiosk` mode pointed at `pi_kiosk_url` and relaunches it if it ever exits.
|
||||
If X itself crashes, `.bash_profile` restarts it.
|
||||
|
||||
Vars:
|
||||
|
||||
- `pi_kiosk_user` (default `kiosk`) - the account that autologs into tty1.
|
||||
- `pi_kiosk_url` (default `http://localhost:8080`) - what Firefox shows.
|
||||
|
||||
Meant to run alongside `pi_musicmouse` on the same host when it's serving the page
|
||||
locally (the default `pi_kiosk_url`), gated in `mediapis.yml` by `mediapi_has_monitor`.
|
||||
|
||||
niri was considered instead of plain X/no-WM, but isn't packaged for Debian Bookworm
|
||||
yet (no official apt package, only third-party `.deb`s or a from-source Rust build) -
|
||||
revisit once it is.
|
||||
3
roles/pi_kiosk/defaults/main.yml
Normal file
3
roles/pi_kiosk/defaults/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
pi_kiosk_user: "kiosk"
|
||||
pi_kiosk_url: "http://localhost:8080"
|
||||
29
roles/pi_kiosk/files/policies.json
Normal file
29
roles/pi_kiosk/files/policies.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"policies": {
|
||||
"DisableAppUpdate": true,
|
||||
"DisableFirefoxStudies": true,
|
||||
"DisableProfileImport": true,
|
||||
"DisableTelemetry": true,
|
||||
"DontCheckDefaultBrowser": true,
|
||||
"NoDefaultBookmarks": true,
|
||||
"OverrideFirstRunPage": "",
|
||||
"OverridePostUpdatePage": "",
|
||||
"PasswordManagerEnabled": false,
|
||||
"UserMessaging": {
|
||||
"WhatsNew": false,
|
||||
"ExtensionRecommendations": false,
|
||||
"FeatureRecommendations": false,
|
||||
"Locked": true
|
||||
},
|
||||
"Preferences": {
|
||||
"browser.sessionstore.resume_from_crash": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
},
|
||||
"browser.tabs.warnOnClose": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
roles/pi_kiosk/handlers/main.yml
Normal file
6
roles/pi_kiosk/handlers/main.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Reload systemd and restart getty
|
||||
ansible.builtin.systemd:
|
||||
name: "getty@tty1"
|
||||
state: restarted
|
||||
daemon_reload: "yes"
|
||||
59
roles/pi_kiosk/tasks/main.yml
Normal file
59
roles/pi_kiosk/tasks/main.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
- name: Packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- xserver-xorg
|
||||
- xinit
|
||||
- x11-xserver-utils
|
||||
- firefox-esr
|
||||
- unclutter
|
||||
cache_valid_time: 7200
|
||||
|
||||
- name: Create kiosk user
|
||||
ansible.builtin.user:
|
||||
name: "{{ pi_kiosk_user }}"
|
||||
groups: [video, input, tty]
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
|
||||
- name: Autologin the kiosk user on tty1
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/getty@tty1.service.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Install getty autologin override
|
||||
ansible.builtin.template:
|
||||
src: autologin.conf.j2
|
||||
dest: /etc/systemd/system/getty@tty1.service.d/autologin.conf
|
||||
mode: "0644"
|
||||
notify: Reload systemd and restart getty
|
||||
|
||||
- name: Install .bash_profile (starts X on tty1 login)
|
||||
ansible.builtin.template:
|
||||
src: bash_profile.j2
|
||||
dest: "/home/{{ pi_kiosk_user }}/.bash_profile"
|
||||
owner: "{{ pi_kiosk_user }}"
|
||||
group: "{{ pi_kiosk_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Install .xinitrc (launches Firefox kiosk, no window manager)
|
||||
ansible.builtin.template:
|
||||
src: xinitrc.j2
|
||||
dest: "/home/{{ pi_kiosk_user }}/.xinitrc"
|
||||
owner: "{{ pi_kiosk_user }}"
|
||||
group: "{{ pi_kiosk_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure firefox-esr policy directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/firefox-esr
|
||||
state: directory
|
||||
mode: "0755"
|
||||
# apt normally creates this; a backstop in case the package layout doesn't
|
||||
|
||||
- name: Install Firefox enterprise policy (suppress session-restore/update prompts)
|
||||
ansible.builtin.copy:
|
||||
src: policies.json
|
||||
dest: /etc/firefox-esr/policies.json
|
||||
mode: "0644"
|
||||
3
roles/pi_kiosk/templates/autologin.conf.j2
Normal file
3
roles/pi_kiosk/templates/autologin.conf.j2
Normal file
@@ -0,0 +1,3 @@
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin {{ pi_kiosk_user }} --noclear %I $TERM
|
||||
8
roles/pi_kiosk/templates/bash_profile.j2
Normal file
8
roles/pi_kiosk/templates/bash_profile.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
# Managed by ansible (pi_kiosk role). Auto-starts the kiosk browser on the console
|
||||
# this account autologs into - not meant for interactive use.
|
||||
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
|
||||
while true; do
|
||||
startx -- -nocursor
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
15
roles/pi_kiosk/templates/xinitrc.j2
Normal file
15
roles/pi_kiosk/templates/xinitrc.j2
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# Managed by ansible (pi_kiosk role). No window manager on purpose - Firefox is the
|
||||
# only X client, launched full-screen in kiosk mode; the loop is what takes the place
|
||||
# of a WM's "keep something on screen" job if it crashes.
|
||||
|
||||
xset s off
|
||||
xset -dpms
|
||||
xset s noblank
|
||||
|
||||
unclutter -idle 0.5 -root &
|
||||
|
||||
while true; do
|
||||
firefox --kiosk "{{ pi_kiosk_url }}"
|
||||
sleep 2
|
||||
done
|
||||
@@ -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).
|
||||
|
||||
15
roles/pi_musicmouse/defaults/main.yml
Normal file
15
roles/pi_musicmouse/defaults/main.yml
Normal 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"
|
||||
29
roles/pi_musicmouse/files/config-musicdolphin.yml
Normal file
29
roles/pi_musicmouse/files/config-musicdolphin.yml
Normal 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"]
|
||||
@@ -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:
|
||||
@@ -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
|
||||
38
roles/pi_musicmouse/handlers/main.yml
Normal file
38
roles/pi_musicmouse/handlers/main.yml
Normal 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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user