Compare commits

...

14 Commits

Author SHA1 Message Date
84b0470b49 Connect musicdolphin's backend to irserver for the IR remote
The remote did nothing on musicdolphin: irserver received the keys, but the backend's
config had no lirc: section, on the belief that irserver could not speak lircd's
protocol. It can - irserver serves the lircd network protocol on port 8765, with lines
the backend's parser reads as they are - so point the backend there. The remote name
must match irserver's remote file exactly: "hauppauge", not lircd's "Hauppauge".

config.yml is install-once, so the running host was changed by hand the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:25:02 +02:00
6423c134be Image script: trixie arm64, NetworkManager wifi, any free loop device
- Download the 2026-09-15 Raspberry Pi OS trixie arm64 lite image.
- Trixie (and NetworkManager-based bookworm) ignore wpa_supplicant.conf on the boot
  partition: write a NetworkManager profile there instead, and set the regulatory
  domain on the kernel command line. The SSID is now asked for too.
- Use whichever loop device is free rather than a hard-coded /dev/loop42, mount into a
  temporary directory, and unmount and detach on any exit. Resolve paths from the
  script's own location, and refuse to overwrite an already customised image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:15:48 +02:00
f2f50e98e1 Build shairport-sync 4.3.7 from its tag
The task passed the script `${shairport_sync_version}` - shell syntax in a Jinja field,
and the wrong variable name - which the Pi's shell expanded to nothing, so the script
fell back to its own default. Its `git reset` then ran outside the fresh clone, failed,
and was ignored without `set -e`: every build was simply upstream master. The script
now takes `pi_shairport_sync_version`, requires the argument, clones that tag directly
and stops on the first error. Bumped to 4.3.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:15:48 +02:00
cb0ece0478 Keep the media-pi roles working on trixie
- pi_hifiberry_amp: the boot config lives at /boot/firmware/config.txt.
- pi_sysdweb: install python3-dbus and python3-systemd from apt, which trixie ships at
  the versions sysdweb needs; bookworm gets the headers pip needs to build dbus-python
  1.4 itself.
- pi_squeezeserver: trixie images have no iptables, and so cannot have the port 80
  redirect that absent.yml removes - skip that step there instead of failing.
- pi_modern_shell_env, pi_slint_frontend: read facts through `ansible_facts` rather than
  the deprecated injected `ansible_*` variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:14:18 +02:00
e3722d2648 Detect a Pi 5 for its kernel instead of a per-host flag
`pi_standard_setup_install_pi5_kernel` now defaults to "auto": the Pi 5 kernel is
installed when the device tree says Raspberry Pi 5, which covers a card moved over from
a Pi 4 and is a no-op on current images that ship it. true/false still force it, and
musicdolphin no longer needs its own override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:14:18 +02:00
a6d4180b2b Build squeezelite on the device on a 64-bit or trixie userland
The shipped squeezelite is a 32-bit ARM binary linked against libFLAC.so.12 (bookworm
on armhf). Elsewhere the role now builds upstream squeezelite at a pinned commit with
the same options as the old ARM build, installing the -dev packages instead of
soname-specific runtime libraries. `ansible_architecture` is the kernel's, so the
choice reads the userland's architecture from dpkg; override with
`pi_squeezelite_custom_build_from_source: true|false`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:14:18 +02:00
f6a169afaa Build irserver on the device on a 64-bit or trixie userland
The shipped irserver is a 32-bit ARM binary linked against libzip.so.4, which only
exists on armhf with Debian 12 or older. On a 64-bit userland or trixie it cannot run,
so the role now compiles the bundled 2019 sources on the device there, with GCC 14's
new hard errors (implicit declarations, implicit int, pointer/int mix-ups) downgraded
rather than the old code patched. Picked from `dpkg --print-architecture` and the
distribution version; `pi_irserver_build_from_source: true|false` forces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:14:18 +02:00
a0546f5fa6 Recover from half-done installs and missing devices instead of failing for good
- pi_shairport: on trixie systemd.pc moved into systemd-dev, so configure found no
  unit directory and `make install` died on an empty `install -d` - after it had
  already installed the binary. `creates:` then saw the binary and never retried, and
  the next tasks failed on the missing unit. Install systemd-dev on trixie, and key
  `creates:` on the unit file.
- pi_irserver: with the IRTrans receiver unplugged at boot, irserver exited at once and
  stayed failed. Restart it every 10s with no start limit until the device shows up.
- pi_modern_shell_env: get_url sent a conditional request to the oh-my-posh CDN on every
  run, so a TLS handshake timeout there failed the whole play with the binary already
  installed. Only download it when it is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:22:05 +02:00
c3375bea75 Deliver the MusicMouse front-end even when a later role fails
The front-end build (npm ci, build, rsync, stamp) runs as handlers, and a failure in
any role after pi_musicmouse threw the queue away. On musicdolphin that left the
backend serving the API only, with no web/dist, across several runs. Flush the
handlers at the end of the role, so the front-end depends on this role succeeding and
not on every role after it.

Also install ffmpeg, which the backend needs to pull the audio out of podcast feeds
that publish video episodes ("Wissen macht Ah!"), and chromium.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:21:55 +02:00
6b9b50ef2e Get sound, the screen and emoji working on musicdolphin's Pi 5
- HiFiBerry: the plain hifiberry-dacplus overlay makes the Pi the I2S clock consumer,
  which the Pi 5's controller refuses (snd_soc_dai_set_fmt -22). The card never probed,
  so every softvol_* device was dead: squeezelite crash-looped and the backend and
  shairport played into nothing. hifiberry-dacplus-std has the Pi drive the clocks.
  pi_hifiberry_amp now matches any dtoverlay=hifiberry-* line, so changing the overlay
  replaces the old one instead of adding a second.
- X: with two DRM devices (v3d, vc4) Xorg gave up with "Cannot run in framebuffer
  mode", and the kiosk looped on a blank screen. 99-vc4.conf points it at vc4, the
  display controller; rendering still goes to V3D (glxinfo: V3D 7.1, accelerated).
- The front-end's icons are emoji in the markup. The Lite image has no emoji font, so
  they rendered as empty boxes: install fonts-noto-color-emoji.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:21:47 +02:00
874a81f664 Give each role's reboot handler its own name
pi_standard_setup, pi_hifiberry_amp and pi_disable_onboard_bluetooth each defined a
handler called "Reboot". Handler names are play-global, so a notify ran only the last
one defined - pi_disable_onboard_bluetooth's - and with that role's `when:`. On every
host where onboard Bluetooth stays on, the handler was skipped and nothing that asked
for a reboot ever got one: not a changed dtoverlay, not a hostname change, not the Pi 5
kernel. Seen on musicdolphin, where a new HiFiBerry overlay sat unapplied in
config.txt after a clean run.

Each role now notifies a handler only it defines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:21:37 +02:00
f434429dd1 Switch musicdolphin from a Pi 4 to a Pi 5
The SD card moves over as is, so:
- pi_standard_setup can install the Pi 5 kernel (linux-image-rpi-2712, an arm64
  package on the armhf userland), which a Pi 4 image lacks. Opt-in via
  pi_standard_setup_install_pi5_kernel.
- The Pi is powered through the HiFiBerry board's GPIO header, so it cannot negotiate
  a 5A supply and would cap USB at 600mA. pi_hifiberry_amp sets
  usb_max_current_enable=1 when hifiberry_powers_pi is true (default false).
- The DHT22 poller is off: Adafruit_DHT bit-bangs BCM283x registers, which the Pi 5
  no longer has (GPIO sits behind the RP1 chip).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 12:17:53 +02:00
ed7b14dcbb Don't deploy the Slint front-end to musicdolphin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:42:40 +02:00
2fb290b146 Build and install the native front-end on musicdolphin
slint-frontend/ in the app repo is a native front-end for the same backend, written
because the React UI is sluggish on this device. This installs it beside the web UI,
never instead of it: the systemd unit is installed but not enabled, so the device still
boots into the kiosk browser and the two can be compared on the same screen.

  systemctl start musikdelfin     # takes over the display
  systemctl stop  musikdelfin     # back to the browser underneath

Unlike pi_musicmouse, which builds the web front-end on the control machine and rsyncs
dist/, this builds on the device. A Rust binary is not portable, and the reason it has
to be built here rather than cross-compiled is worth writing down: these Pis run 32-bit
armhf Raspbian on a 64-bit kernel. `uname -m` says aarch64 and it is misleading;
`dpkg --print-architecture` says armhf and that is the one that matters. Cross-compiling
to armv7-unknown-linux-gnueabihf needs a sysroot, the devices carry no -dev packages to
build one from, and reconstructing one from the Debian archive means owning a glibc skew
risk indefinitely. Spending the device's own CPU once is the smaller liability. The
build is stamped with the commit it came from, so it only runs again when that changes.

The same fact rules out the Skia renderer that the app's own kiosk profile prefers:
rust-skia publishes no prebuilt binaries for armhf. This builds --features desktop -
winit, FemtoVG and the software renderer - and runs as an X client in the existing
session. Worth noting after the fact: the FemtoVG bug that makes Skia the upstream
recommendation (slint-ui/slint#11785, album covers drawn as solid black) was reported on
a Pi 5 with a 64-bit userland and does not reproduce here - covers render correctly.

Three things had to be found by running it rather than reading anything:

The X session belongs to `kiosk`, not root, so the launcher needs that user's cookie and
the unit runs as them. Without it winit fails with "could not initialize backend", which
says nothing about authorization.

winit and glutin dlopen every windowing and GL library they use, so none of them appear
in `ldd` and the binary links happily without them - then aborts on first run with
"Library libxkbcommon-x11.so could not be loaded". The runtime packages are listed
explicitly for that reason.

Raspbian ships no font containing the emoji the design uses as iconography, so the shelf
badges rendered as empty boxes until fonts-noto-color-emoji went in.

First build took 63 minutes: 570 crates, two jobs, nice'd, which bounds peak memory on
musicdolphin's 1.8 GB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 00:44:54 +02:00
37 changed files with 733 additions and 80 deletions

View File

@@ -5,6 +5,11 @@ mediapi_has_dhtsensor: false
mediapi_disable_onboard_bluetooth: false
mediapi_install_bluetooth_monitor: false
mediapi_install_kidsmusic: false
# The native Slint front-end, built on the device and installed beside the web UI.
# Needs mediapi_install_kidsmusic, which owns the checkout it builds from. Off by
# default: the build is ~40 minutes of the device's own CPU the first time.
mediapi_install_slint_frontend: false
mediapi_has_monitor: false
# No media pi runs its own Logitech Media Server. Every squeezelite in the fleet is

View File

@@ -4,16 +4,29 @@ pi_shairport_name: _Oben_Wohnzimmer
alsa_card_name: sndrpihifiberry
sensor_room_name_ascii: wohnzimmeroben
sensor_room_name: WohnzimmerOben
hifiberry_overlay: hifiberry-dacplus
# -std: the Pi drives the I2S clocks. The plain dacplus overlay makes the Pi the clock
# consumer, which the Pi 5's I2S controller rejects (snd_soc_dai_set_fmt -22), so the
# card never probes and every softvol_* device is dead.
hifiberry_overlay: hifiberry-dacplus-std
main_user: root
mediapi_has_hifiberry_amp: true
mediapi_ir_control: irserver
mediapi_has_dhtsensor: true
# Off for the Pi 5: the legacy Adafruit_DHT library bit-bangs the BCM283x registers,
# which the Pi 5 no longer has (GPIO sits behind the RP1 chip), so the poller cannot
# work there. Needs a libgpiod-based replacement before this goes back to true.
mediapi_has_dhtsensor: false
# Raspberry Pi 5 (detected from the device tree; the Pi 5 kernel is installed as needed).
# The Pi is fed 5V through the HiFiBerry board's GPIO header, not USB-C, so it cannot
# negotiate a 5A supply and would cap USB current at 600mA.
hifiberry_powers_pi: 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
# The native Slint front-end is not deployed here; the browser kiosk is the front-end.
mediapi_install_slint_frontend: false
mediapi_has_monitor: true
# Temporarily a debuggable desktop rather than a locked-down kiosk: Openbox root menu,
# visible pointer, Firefox in a normal window. Set back to "kiosk" when done.

View File

@@ -9,6 +9,8 @@
when: mediapi_has_hifiberry_amp
- role: pi_musicmouse
when: mediapi_install_kidsmusic
- role: pi_slint_frontend
when: mediapi_install_slint_frontend
- role: pi_kiosk
when: mediapi_has_monitor
- pi_squeezelite_custom

View File

@@ -1,3 +1,7 @@
---
- name: Reboot
# Handler names are play-global: if several roles define the same name, only the
# last one defined runs, under *that* role's `when:`. A shared "Reboot" therefore
# never fired while pi_disable_onboard_bluetooth (usually switched off) defined one
# too. Keep this name unique to the role.
- name: Reboot after disabling onboard Bluetooth
ansible.builtin.reboot:

View File

@@ -4,4 +4,4 @@
path: /boot/firmware/config.txt
regexp: "^#?dtoverlay=disable-bt"
line: "dtoverlay=disable-bt"
notify: Reboot
notify: Reboot after disabling onboard Bluetooth

View File

@@ -3,4 +3,7 @@
Deactivates the Pi's onboard analog audio and activates a HiFiBerry
amp/DAC overlay instead.
**Key vars:** `hifiberry_overlay`
**Key vars:** `hifiberry_overlay`, `hifiberry_powers_pi`
`hifiberry_powers_pi` (default `false`) is for a Pi fed through the HiFiBerry board's
GPIO header. On a Pi 5 that sets `usb_max_current_enable=1`, or USB is capped at 600mA.

View File

@@ -0,0 +1,2 @@
---
hifiberry_powers_pi: false

View File

@@ -1,3 +1,7 @@
---
- name: Reboot
# Handler names are play-global: if several roles define the same name, only the
# last one defined runs, under *that* role's `when:`. A shared "Reboot" therefore
# never fired while pi_disable_onboard_bluetooth (usually switched off) defined one
# too. Keep this name unique to the role.
- name: Reboot after HiFiBerry boot config change
ansible.builtin.reboot:

View File

@@ -1,13 +1,25 @@
---
- name: Deactivate normal audio
ansible.builtin.lineinfile:
path: /boot/config.txt
path: /boot/firmware/config.txt
regexp: "^#?dtparam=audio=on"
line: "#dtparam=audio=on"
notify: Reboot
notify: Reboot after HiFiBerry boot config change
- name: Activate Hifiberry
ansible.builtin.lineinfile:
path: /boot/firmware/config.txt
regexp: "^#?dtoverlay=hifiberry-amp"
# Any hifiberry overlay, not just the one this used to set: otherwise changing
# hifiberry_overlay appends a second dtoverlay line and leaves the old one active.
regexp: "^#?dtoverlay=hifiberry-"
line: "dtoverlay={{ hifiberry_overlay }}"
notify: Reboot
notify: Reboot after HiFiBerry boot config change
- name: Lift the Pi 5's USB current cap when the HiFiBerry board powers it
# A Pi 5 fed over the GPIO header has no USB-C power negotiation, so it assumes a
# weak supply and limits USB ports to 600mA. Unknown to a Pi 4, which ignores it.
ansible.builtin.lineinfile:
path: /boot/firmware/config.txt
regexp: "^#?usb_max_current_enable="
line: "usb_max_current_enable=1"
insertafter: EOF
when: hifiberry_powers_pi
notify: Reboot after HiFiBerry boot config change

View File

@@ -3,3 +3,9 @@
Installs and configures `irserver` (with a Hauppauge remote config) as an
autostarted systemd service for IR remote control, and registers it with
`pi_sysdweb`.
The shipped `files/irserver` is a 32-bit ARM binary needing `libzip.so.4`, so it is used
only on an armhf userland with Debian 12 or older. On a 64-bit userland or trixie it is
compiled on the device from `files/irserver-src.tar.gz` instead (the 2019 sources need
GCC's newer hard errors downgraded; see `tasks/build.yml`). Automatic; override with
`pi_irserver_build_from_source: true|false`.

View File

@@ -0,0 +1,6 @@
---
# The shipped files/irserver is a 32-bit ARM binary linked against libzip.so.4 (Debian
# bookworm's libzip4 on armhf). It cannot run on a 64-bit userland, and trixie no longer
# ships libzip.so.4 at all. "auto" uses it only on armhf + Debian <= 12 and compiles
# files/irserver-src.tar.gz on the device everywhere else; set true/false to force.
pi_irserver_build_from_source: auto

View File

@@ -1,10 +1,17 @@
[Unit]
Description=IR server for IR remotes
After=network.target
# Keep retrying rather than giving up: the IRTrans receiver is a USB serial device, and
# when it is missing at boot (unplugged, or enumerated late) irserver exits at once with
# "Error opening COM/USB Port". Without a restart it then stays failed until someone
# notices the remote does nothing.
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=/usr/bin/irserver /dev/ttyUSB0
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target

View File

@@ -0,0 +1,44 @@
---
- name: Install irserver build dependencies
ansible.builtin.apt:
name:
- build-essential
- libzip-dev
cache_valid_time: 7200
- name: Create irserver source directory
ansible.builtin.file:
path: /opt/irserver-src
state: directory
mode: "0755"
- name: Unpack irserver source
ansible.builtin.unarchive:
src: irserver-src.tar.gz
dest: /opt/irserver-src
creates: /opt/irserver-src/makefile
- name: Build irserver
# The 2019 sources predate modern C: GCC 14 rejects implicit function declarations,
# implicit int and a few pointer/int mix-ups as hard errors. They are old but working
# code, so the errors are downgraded rather than the source patched. libzip comes from
# libzip-dev (`irserver_arm_noccf` links -lzip); the bundled 32-bit static libzip*.a
# are not used. `arm/` is where the makefile puts its objects.
ansible.builtin.shell:
cmd: |
set -e
mkdir -p arm
make irserver_arm_noccf LDFLAGS="-lzip" \
CFLAGS="-w -Wno-error=implicit-function-declaration -Wno-error=int-conversion \
-Wno-error=incompatible-pointer-types -Wno-error=return-mismatch \
-Wno-error=implicit-int"
chdir: /opt/irserver-src
creates: /opt/irserver-src/irserver
- name: Install built irserver
ansible.builtin.copy:
src: /opt/irserver-src/irserver
dest: /usr/bin/irserver
mode: u+rx
remote_src: true
notify: Restart irserver

View File

@@ -1,10 +1,27 @@
---
- name: Copy irserver
- name: Get the userland architecture
# See pi_squeezelite_custom: the kernel's architecture is not the userland's.
ansible.builtin.command: dpkg --print-architecture
register: pi_irserver_dpkg_arch
changed_when: false
check_mode: false # read-only
- name: Decide between the prebuilt binary and a build on the device
ansible.builtin.set_fact:
pi_irserver_build: >-
{{ (pi_irserver_build_from_source | string | lower == 'true')
or (pi_irserver_build_from_source | string | lower == 'auto'
and (pi_irserver_dpkg_arch.stdout | trim != 'armhf'
or ansible_facts.distribution_major_version | int >= 13)) }}
- name: Copy prebuilt irserver (32-bit ARM binary)
ansible.builtin.copy:
src: irserver
dest: /usr/bin/irserver
mode: u+rx
notify: Restart irserver
when: not pi_irserver_build
- name: Build irserver from source (needed on a 64-bit userland, see README)
ansible.builtin.include_tasks: build.yml
when: pi_irserver_build
- name: Make config dir for remotes
ansible.builtin.file:
path: /usr/bin/remotes

View File

@@ -10,7 +10,37 @@
# size in the corner of the screen. Openbox answers it, and nothing else.
- openbox
- unclutter
# The MusicMouse front-end's icons are emoji characters in the markup, not an icon
# font. Lite images ship no emoji font at all, so without this every icon renders
# as an empty box.
- fonts-noto-color-emoji
cache_valid_time: 7200
notify: Reload systemd and restart getty
- name: Ensure the Xorg config directory exists
ansible.builtin.file:
path: /etc/X11/xorg.conf.d
state: directory
mode: "0755"
- name: Point X at the display controller
# The Pi 4 and 5 have two DRM devices: v3d (the 3D GPU, no outputs) and vc4 (the
# HDMI display controller). Left to itself Xorg cannot tell which to drive and dies
# with "Cannot run in framebuffer mode. Please specify busIDs for all framebuffer
# devices", so the kiosk loops on a blank screen. Display goes through vc4; rendering
# still lands on v3d, because glamor's GL context is routed there by Mesa (kmsro).
# Raspberry Pi's desktop image ships this same file; the Lite image does not.
ansible.builtin.copy:
dest: /etc/X11/xorg.conf.d/99-vc4.conf
content: |
Section "OutputClass"
Identifier "vc4"
MatchDriver "vc4"
Driver "modesetting"
Option "PrimaryGPU" "true"
EndSection
mode: "0644"
notify: Reload systemd and restart getty
- name: Browser
# Chromium comes from archive.raspberrypi.com, not Debian: that build carries the

View File

@@ -17,14 +17,24 @@
dest: /usr/local/bin/bat
state: link
- name: Check whether oh-my-posh is already installed
ansible.builtin.stat:
path: /usr/local/bin/oh-my-posh
register: pi_modern_shell_env_ohmyposh
- name: Download oh-my-posh binary
# Only when missing. With the file present get_url still sends a conditional request
# every run, so a CDN that is slow or unreachable from the device (TLS handshake
# timeouts happen) failed the whole play for a prompt that was already installed.
# Delete the binary to pick up a newer release.
when: not pi_modern_shell_env_ohmyposh.stat.exists
ansible.builtin.get_url:
# The official install script's own curl-based downloads are blocked by
# oh-my-posh's CDN on some devices (observed on Raspberry Pi OS's curl
# build: TLS handshake fails with SSL_ERROR_SYSCALL), while Ansible's
# get_url (Python urllib) succeeds against the same CDN. Download the
# binary directly instead of shelling out to the install script.
url: "https://cdn.ohmyposh.dev/releases/latest/posh-linux-{{ pi_modern_shell_env_ohmyposh_arch_map[ansible_architecture] }}"
url: "https://cdn.ohmyposh.dev/releases/latest/posh-linux-{{ pi_modern_shell_env_ohmyposh_arch_map[ansible_facts['architecture']] }}"
dest: /usr/local/bin/oh-my-posh
mode: "0755"

View File

@@ -68,10 +68,15 @@ general:
# 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.
# IR remote. This host uses mediapi_ir_control: irserver (the IRTrans receiver on
# /dev/ttyUSB0) rather than lircd, but irserver also serves lircd's network protocol
# on its own port 8765 - which is all the backend's LircConfig needs. The remote name
# is irserver's remote file, /usr/bin/remotes/hauppauge.rem, in lowercase. Number keys
# play whatever the top-level `remote:` mapping assigns them (none yet).
lirc:
host: "127.0.0.1"
port: 8765
remote_name: "hauppauge"
# 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'.

View File

@@ -5,6 +5,8 @@
- git
- libvlc5 # runtime lib python-vlc binds against
- vlc-plugin-base
- ffmpeg # podcast feeds that publish video episodes get their audio extracted with it
- chromium
- alsa-utils
- samba
- python3
@@ -237,6 +239,15 @@
name: smbd
state: started
enabled: true
- name: Build, sync and restart now rather than at the end of the play
# Handlers only run once the whole play is through, and a failure in any later role
# (squeezelite, shairport, ...) throws the queue away. The .built-commit guard above
# re-queues the build on the next run, but a play that keeps failing elsewhere then
# never delivers a front-end at all: the backend serves the API only and the kiosk
# shows nothing. Flushing here ties the front-end to this role succeeding, not to
# every role after it.
ansible.builtin.meta: flush_handlers
# manual steps:
# - set samba passwords with smbpasswd
# - copy music into /media/musicmouse: scripts/sync-library.sh in the app repo, which

View File

@@ -1,3 +1,3 @@
---
pi_shairport_sync_version: "3.3.5"
pi_shairport_sync_version: "4.3.7"
pi_shairport_name: Unnamed Raspberry with shairport

View File

@@ -1,15 +1,14 @@
#!/bin/bash
set -e
ARG1=$1
VERSION=${ARG1:="3.3.6"}
VERSION=${1:?usage: build-shairport-sync.sh <git tag>}
# Make a new temporary directory and go there
tmp_dir=$(mktemp -d)
cd $tmp_dir
cd "$tmp_dir"
# Clone, build and install
git clone https://github.com/mikebrady/shairport-sync.git
git reset --hard origin/$VERSION
# Clone the requested tag, build and install
git clone --depth 1 --branch "$VERSION" https://github.com/mikebrady/shairport-sync.git
cd shairport-sync
autoreconf -fi
./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd
@@ -18,4 +17,4 @@ make install
# Clean up
cd ~
rm -rf $tmp_dir
rm -rf "$tmp_dir"

View File

@@ -3,24 +3,20 @@
ansible.builtin.apt:
cache_valid_time: 7200
state: present
name:
- build-essential
- git
- xmltoman
- autoconf
- automake
- libtool
- libpopt-dev
- libconfig-dev
- libasound2-dev
- avahi-daemon
- libavahi-client-dev
- libssl-dev
- libsoxr-dev
# systemd-dev: --with-systemd takes the unit directory from systemd.pc, which trixie
# moved out of the systemd package. Without it `make install` fails right after
# installing the binary, with an empty `install -d`.
name: >-
{{ ['build-essential', 'git', 'xmltoman', 'autoconf', 'automake', 'libtool',
'libpopt-dev', 'libconfig-dev', 'libasound2-dev', 'avahi-daemon',
'libavahi-client-dev', 'libssl-dev', 'libsoxr-dev']
+ (['systemd-dev'] if ansible_facts.distribution_major_version | int >= 13 else []) }}
- name: Build and Install Shairport sync (may take a while)
ansible.builtin.script: "build-shairport-sync.sh ${shairport_sync_version}"
ansible.builtin.script: "build-shairport-sync.sh {{ pi_shairport_sync_version }}"
args:
creates: /usr/local/bin/shairport-sync
# The unit file rather than the binary: make install puts the binary in place
# first, so a build that failed halfway would otherwise never be retried.
creates: /lib/systemd/system/shairport-sync.service
notify: Restart shairport-sync
- name: Copy config
ansible.builtin.template:

View File

@@ -0,0 +1,44 @@
# pi_slint_frontend
Builds and installs `slint-frontend/` from the app repo - the native front-end that
exists because the React UI is sluggish on the kiosk. Installed *alongside* the web UI,
never instead of it, and the systemd unit is installed but not enabled: the device still
boots into the kiosk browser.
```sh
systemctl start musikdelfin # takes over the screen
systemctl stop musikdelfin # back to the browser underneath
```
or over SSH, `DISPLAY=:0 /usr/local/bin/musikdelfin`.
## Why this builds on the device
`pi_musicmouse` builds the web front-end on the control machine and rsyncs `dist/`,
because npm output is portable and npm is not installed on the Pis. A Rust binary is
not portable, so this role has a harder choice.
The Pis run **32-bit armhf Raspbian on a 64-bit kernel** - `uname -m` says `aarch64`
and `dpkg --print-architecture` says `armhf`, and the second one is the one that
matters. The target is `armv7-unknown-linux-gnueabihf`. Cross-compiling to it from the
control machine needs a sysroot, and the devices carry no `-dev` packages to build one
out of, so it would have to be reconstructed from the Debian archive - and then kept in
step with the devices' glibc forever. Spending the device's own CPU once is the smaller
liability. A rebuild only happens when the commit changes.
The same fact rules out the Skia renderer: rust-skia publishes no prebuilt binaries for
armhf, so `--features kiosk` would mean compiling Skia from source on a Pi. This role
builds `--features desktop` - winit, FemtoVG and the software renderer - and runs as an
X client in the existing session rather than taking over KMS.
`SLINT_BACKEND=winit-software musikdelfin` switches renderer without a rebuild, which
is the thing to try first if the GPU path misbehaves. The FemtoVG bug that makes
`kiosk` use Skia upstream (slint-ui/slint#11785) was reported on a Pi 5 with a 64-bit
userland, so it may well not apply here - but it is worth checking that album covers
actually appear before drawing conclusions about frame rate.
## First run
Roughly 40 minutes: 570 crates on two cores. Later runs are incremental and usually
seconds. The build is `nice`d and limited to two jobs, which bounds peak memory on
musicdolphin's 1.8 GB.

View File

@@ -0,0 +1,42 @@
---
# The native Slint front-end (slint-frontend/ in the app repo). Installed alongside the
# web UI, not instead of it: both talk to the same backend, so the two can be compared
# on the same screen.
# Built on the device rather than on the control machine, which is the opposite of what
# pi_musicmouse does for the web front-end - and for a reason. The Pis run 32-bit armhf
# Raspbian on a 64-bit kernel, so the target is armv7-unknown-linux-gnueabihf. Cross
# compiling to it needs a sysroot, and the device has no -dev packages to build one
# from, so the choice is between reconstructing one from the Debian archive (and
# owning a glibc-version skew risk forever) or spending the device's own CPU once.
# A rebuild only happens when the commit changes - see the stamp below.
pi_slint_frontend_toolchain: stable
# Bounds peak memory during the build. musicdolphin has 4 cores but only 1.8 GB.
pi_slint_frontend_jobs: 2
# winit + FemtoVG + the software renderer. Deliberately not Skia: rust-skia ships no
# prebuilt binaries for armhf, so `kiosk` would mean compiling Skia from source on a
# Pi, which will not finish in any reasonable time and would not fit on the disk.
pi_slint_frontend_features: desktop
# Which backend the app is served by. Same machine as the kiosk browser uses.
pi_slint_frontend_server: "http://localhost:8080"
# Where the checkout lives. Shared with pi_musicmouse, which owns the checkout itself.
pi_slint_frontend_root: /opt/musicmouse/slint-frontend
# The user the kiosk's X session belongs to - the app has to run as them to reach the
# display. pi_kiosk owns this name; repeated here as a default so the role still works
# on a host where pi_kiosk has not run.
pi_slint_frontend_x_user: "{{ pi_kiosk_user | default('kiosk') }}"
# The device is a kiosk, so the window takes the whole screen. Set false to get a
# normal window - useful while pi_kiosk_mode is "debug" and you want the browser and
# this side by side on the same display.
pi_slint_frontend_fullscreen: true
# Slint's own frame-timing readout, for measuring rather than guessing. Empty to leave
# it off; "overlay,refresh_lazy" draws the numbers on screen and only counts frames
# that were actually rendered, which is the honest measure for a UI that idles.
pi_slint_frontend_debug_performance: ""

View File

@@ -0,0 +1,4 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true

View File

@@ -0,0 +1,143 @@
---
- name: Packages needed to build the front-end
ansible.builtin.apt:
name:
- build-essential
- pkg-config
# The only C libraries the binary links: everything else - X11, Wayland, EGL,
# GL - is dlopen'd at runtime by winit and glutin, so it is not needed to build.
- libfontconfig1-dev
- libfreetype-dev
- curl
# Runtime, not build: winit and glutin dlopen all of these, so they never show
# up in `ldd` and the binary links happily without them - it then aborts on
# first run with "Library libxkbcommon-x11.so could not be loaded".
- libxkbcommon0
- libxkbcommon-x11-0
- libx11-xcb1
- libxcb-xkb1
- libxcursor1
- libxi6
- libxrandr2
- libgl1
- libegl1
- libgles2
# The design uses emoji as iconography - the shelf badges, the figurine marker,
# the scene sparkle. Raspbian ships no font that has them, so without these they
# render as empty boxes. Symbola is the monochrome fallback for the glyphs Noto
# does not cover.
- fonts-noto-color-emoji
- fonts-symbola
cache_valid_time: 7200
- name: Check for a rust toolchain
ansible.builtin.stat:
path: /root/.cargo/bin/cargo
register: pi_slint_frontend_cargo
- name: Fetch the rustup installer
ansible.builtin.get_url:
url: https://sh.rustup.rs
dest: /tmp/rustup-init.sh
mode: "0755"
when: not pi_slint_frontend_cargo.stat.exists
- name: Install the rust toolchain
# Debian 12 ships rustc 1.63, which is far too old for Slint, so this comes from
# rustup rather than apt.
ansible.builtin.command:
cmd: "/tmp/rustup-init.sh -y --profile minimal --default-toolchain {{ pi_slint_frontend_toolchain }}"
creates: /root/.cargo/bin/cargo
when: not pi_slint_frontend_cargo.stat.exists
- name: Check the checkout has a slint-frontend
ansible.builtin.stat:
path: "{{ pi_slint_frontend_root }}/Cargo.toml"
register: pi_slint_frontend_manifest
when: not ansible_check_mode
- name: Fail on a checkout without the front-end
ansible.builtin.fail:
msg: >-
{{ musicmouse_version }} has no slint-frontend/, so it predates the native
front-end. Point musicmouse_version at a branch that has it, or set
mediapi_install_slint_frontend to false for this host.
when: not ansible_check_mode and not pi_slint_frontend_manifest.stat.exists
- name: Fail when the checkout's commit is unknown
# This role builds what pi_musicmouse checked out, and reuses its record of which
# commit that was. With mediapi_install_kidsmusic false that role never ran, so
# there is no checkout to build and no commit to stamp - and the stamp comparison
# below would compare against an empty string and rebuild on every run.
ansible.builtin.fail:
msg: >-
pi_slint_frontend needs pi_musicmouse to have run first - it builds the
slint-frontend/ inside the checkout that role owns. Set
mediapi_install_kidsmusic to true for this host, or
mediapi_install_slint_frontend to false.
when: not ansible_check_mode and pi_musicmouse_commit | length == 0
- name: Read which commit the binary was built from
# The same guard pi_musicmouse uses for the venv and the web dist, for the same
# reason: hanging a 40-minute build off "the checkout changed" strands the device
# when a run fails after the checkout, because the next run finds it current and
# rebuilds nothing.
ansible.builtin.command:
cmd: "cat {{ pi_slint_frontend_root }}/.built-commit"
register: pi_slint_frontend_stamp
changed_when: false
failed_when: false
- name: Build the front-end
# First build takes roughly 40 minutes on a Pi 4 - 570 crates, two jobs. Later ones
# are incremental. `async` keeps the SSH connection from idling out underneath it.
ansible.builtin.command:
cmd: >-
nice -n 10 /root/.cargo/bin/cargo build --release
--no-default-features --features {{ pi_slint_frontend_features }}
-j {{ pi_slint_frontend_jobs }}
chdir: "{{ pi_slint_frontend_root }}"
environment:
# The committed release profile is tuned for a workstation. Thin LTO with
# codegen-units=1 is a memory risk on 1.8 GB and buys nothing that matters here.
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16"
PATH: "/root/.cargo/bin:{{ ansible_facts['env']['PATH'] }}"
async: 7200
poll: 30
changed_when: true
when: >-
not ansible_check_mode
and pi_slint_frontend_stamp.stdout | trim != pi_musicmouse_commit
- name: Record which commit the binary was built from
ansible.builtin.copy:
content: "{{ pi_musicmouse_commit }}\n"
dest: "{{ pi_slint_frontend_root }}/.built-commit"
mode: "0644"
when: not ansible_check_mode
- name: Find the X session user's home directory
ansible.builtin.getent:
database: passwd
key: "{{ pi_slint_frontend_x_user }}"
- name: Note where the X cookie lives
ansible.builtin.set_fact:
pi_slint_frontend_x_home: "{{ ansible_facts.getent_passwd[pi_slint_frontend_x_user][4] }}"
- name: Install the launcher
ansible.builtin.template:
src: musikdelfin.sh.j2
dest: /usr/local/bin/musikdelfin
mode: "0755"
- name: Install the systemd unit
# Installed but deliberately not enabled: this is the thing under test, and the
# kiosk browser is still what the device shows on boot. Start it by hand -
# `systemctl start musikdelfin` - or run /usr/local/bin/musikdelfin over SSH.
ansible.builtin.template:
src: musikdelfin.service.j2
dest: /etc/systemd/system/musikdelfin.service
mode: "0644"
notify: Reload systemd

View File

@@ -0,0 +1,25 @@
# {{ ansible_managed }}
#
# Installed, not enabled. The kiosk browser is still what this device shows on boot;
# this unit is here so the native front-end can be started and stopped the same way
# while it is being evaluated:
#
# systemctl start musikdelfin # takes over the screen
# systemctl stop musikdelfin # back to the browser underneath
[Unit]
Description=Musik Delfin (native Slint front-end)
After=musicmouse.service graphical.target
Wants=musicmouse.service
[Service]
Type=simple
# Runs as the X session's own user, not root: the display belongs to them, and the
# cover cache should land in their home rather than root's.
User={{ pi_slint_frontend_x_user }}
ExecStart=/usr/local/bin/musikdelfin
Restart=no
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=graphical.target

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# {{ ansible_managed }}
#
# Run the native front-end on the kiosk's X display. The web UI stays where it is;
# both talk to the same backend, so they can be compared on the same screen.
#
# musikdelfin the display, as the kiosk uses it
# SLINT_BACKEND=winit-software musikdelfin no GPU, for comparison
# SLINT_FULLSCREEN=0 musikdelfin a normal window, beside the browser
# SLINT_DEBUG_PERFORMANCE=overlay,refresh_lazy musikdelfin frame timings on screen
set -eu
# The kiosk's X session belongs to {{ pi_slint_frontend_x_user }}, and its cookie is
# the only way onto that display - without it the connection is refused before winit
# gets far enough to report anything more useful than "could not initialize backend".
export DISPLAY="${DISPLAY:-:0}"
export XAUTHORITY="${XAUTHORITY:-{{ pi_slint_frontend_x_home }}/.Xauthority}"
# Nunito is the face the design is drawn in; without it Slint falls back to whatever
# fontconfig hands back, which changes how this looks but not whether it runs.
for candidate in \
/usr/share/fonts/truetype/nunito/Nunito[wght].ttf \
/usr/share/fonts/truetype/nunito/Nunito-Regular.ttf \
{{ pi_slint_frontend_root }}/fonts/Nunito.ttf
do
if [ -f "$candidate" ]; then
export SLINT_DEFAULT_FONT="$candidate"
break
fi
done
{% if pi_slint_frontend_fullscreen %}
export SLINT_FULLSCREEN="${SLINT_FULLSCREEN:-1}"
{% endif %}
{% if pi_slint_frontend_debug_performance %}
export SLINT_DEBUG_PERFORMANCE="${SLINT_DEBUG_PERFORMANCE:-{{ pi_slint_frontend_debug_performance }}}"
{% endif %}
exec {{ pi_slint_frontend_root }}/target/release/musicmouse-slint \
--server {{ pi_slint_frontend_server }} "$@"

View File

@@ -3,3 +3,9 @@
Like `pi_squeezelite`, but uninstalls the distro package and deploys a
custom-compiled `squeezelite` build instead as a systemd service. See
`custom build/readme_build.md` for how that build is produced.
The shipped 32-bit binary only runs on an armhf userland with Debian 12 (bookworm) or
older. On a 64-bit userland or Debian 13 (trixie) the role builds the same upstream commit
on the device and installs it to `/opt/squeezelite` instead. That is picked
automatically from `dpkg --print-architecture` and the distribution version; override with
`pi_squeezelite_custom_build_from_source: true|false`.

View File

@@ -0,0 +1,34 @@
---
# The shipped files/squeezelite is a 32-bit ARM binary linked against libFLAC.so.12, which
# is what Debian bookworm on armhf provides. It cannot run on a 64-bit userland or on
# trixie (libflac14), so by default ("auto") the role uses it only on armhf + Debian <= 12
# and compiles pi_squeezelite_custom_version on the device everywhere else. Set true/false
# to force one or the other.
pi_squeezelite_custom_build_from_source: auto
pi_squeezelite_custom_repo: https://github.com/ralph-irving/squeezelite.git
# Upstream master when this was written (squeezelite 2.0, revision 1595). The commit the
# prebuilt binary came from (c89faf3 in the old build notes) does not exist upstream.
pi_squeezelite_custom_version: c7c4248ddd70e47dbfeba0bf4a8a7ec08d8a995c
# Runtime libraries the prebuilt binary links (bookworm package names).
pi_squeezelite_custom_runtime_packages:
- libmad0
- libmpg123-0
- libflac12
- libvorbisfile3
- libfaad2
# Headers (and, through them, the runtime libraries) for building. Installing the -dev
# packages is what makes this independent of soname bumps between Debian releases.
pi_squeezelite_custom_build_packages:
- build-essential
- git
- libasound2-dev
- libssl-dev
- libsoxr-dev
- libflac-dev
- libvorbis-dev
- libmad0-dev
- libfaad-dev
- libmpg123-dev

View File

@@ -0,0 +1,41 @@
---
- name: Check out squeezelite
ansible.builtin.git:
repo: "{{ pi_squeezelite_custom_repo }}"
dest: /opt/squeezelite-src
version: "{{ pi_squeezelite_custom_version }}"
force: true
register: pi_squeezelite_custom_checkout
- name: Check whether a built binary already exists
ansible.builtin.stat:
path: /opt/squeezelite
register: pi_squeezelite_custom_binary
- name: Write the makefile (upstream Makefile plus this build's options)
# Same options as the ARM build in "custom build/readme_build.md", minus the ARMv6
# -march/-mfpu/-mfloat-abi flags that make no sense for aarch64. No RPI (GPIO switching)
# option. -lasound is added explicitly, as before.
ansible.builtin.copy:
dest: /opt/squeezelite-src/mymakefile
mode: "0644"
content: |
OPTS = -DLINUX -DRESAMPLE -DDSD -DUSE_SSL -DLINKALL -I./include -I./include/opus -I./include/alac -I/usr/local/include
LDFLAGS = -L./lib -L/usr/local/lib -s -lasound
include Makefile
- name: Build squeezelite
ansible.builtin.shell:
cmd: make -f mymakefile clean && make -f mymakefile
chdir: /opt/squeezelite-src
when: pi_squeezelite_custom_checkout.changed or not pi_squeezelite_custom_binary.stat.exists
changed_when: true
- name: Install built squeezelite
ansible.builtin.copy:
src: /opt/squeezelite-src/squeezelite
dest: /opt/squeezelite
mode: "700"
remote_src: true
notify: Restart squeezelite

View File

@@ -3,15 +3,23 @@
ansible.builtin.apt:
name: squeezelite
state: absent
- name: Get the userland architecture
# `ansible_architecture` is the *kernel's*: a Pi on a 64-bit kernel with an armhf
# userland says aarch64. What decides whether a 32-bit binary runs is dpkg's.
ansible.builtin.command: dpkg --print-architecture
register: pi_squeezelite_custom_dpkg_arch
changed_when: false
check_mode: false # read-only
- name: Decide between the prebuilt binary and a build on the device
ansible.builtin.set_fact:
pi_squeezelite_custom_build: >-
{{ (pi_squeezelite_custom_build_from_source | string | lower == 'true')
or (pi_squeezelite_custom_build_from_source | string | lower == 'auto'
and (pi_squeezelite_custom_dpkg_arch.stdout | trim != 'armhf'
or ansible_facts.distribution_major_version | int >= 13)) }}
- name: Install dependencies
ansible.builtin.apt:
name:
- libmad0
- libmpg123-0
- libflac12
- libvorbisfile3
- libfaad2
# for building libssl-dev, libasound2-dev, libflac-dev, libvorbis-dev, libsoxr-dev, libfaad-dev, libmad0-dev, libmpg123-dev
name: "{{ pi_squeezelite_custom_build_packages if pi_squeezelite_custom_build else pi_squeezelite_custom_runtime_packages }}"
state: present
cache_valid_time: 7200
- name: Remove old config file if present
@@ -24,6 +32,10 @@
dest: /opt/squeezelite
mode: "700"
notify: Restart squeezelite
when: not pi_squeezelite_custom_build
- name: Build squeezelite from source (needed on a 64-bit userland, see README)
ansible.builtin.include_tasks: build.yml
when: pi_squeezelite_custom_build
- name: Install systemd service file
ansible.builtin.template:
src: squeezelite.service

View File

@@ -36,7 +36,17 @@
state: absent
notify: Restart sysdweb after dropping the LMS entry
- name: Check whether iptables is installed
# Trixie images ship nftables only. A host without iptables cannot have the redirect
# below (present.yml installs iptables-persistent), so there is nothing to undo.
ansible.builtin.command: which iptables
register: pi_squeezeserver_iptables
changed_when: false
failed_when: false
check_mode: false # read-only
- name: Undo the port 80 -> 9000 redirect the install added
when: pi_squeezeserver_iptables.rc == 0
ansible.builtin.iptables:
table: nat
chain: PREROUTING

View File

@@ -9,3 +9,9 @@ pi_standard_setup_wifi_pass_url: "bauer_wifi" # has to be in keepass with url "w
ansible_ssh_pass: "raspberry"
ansible_become_password: "raspberry"
ansible_become: true
# Install the Pi 5 (BCM2712) kernel, kernel_2712.img (an arm64 package, hence the :arm64 on
# an armhf userland). A card imaged on a Pi 4 does not have it, and a Pi 5 will not boot
# without. "auto" installs it when the detected model is a Pi 5 (a no-op on current
# images, which ship it); set true/false to force.
pi_standard_setup_install_pi5_kernel: auto

View File

@@ -3,7 +3,11 @@
ansible.builtin.service:
name: sshd
state: restarted
- name: Reboot
# Handler names are play-global: if several roles define the same name, only the
# last one defined runs, under *that* role's `when:`. A shared "Reboot" therefore
# never fired while pi_disable_onboard_bluetooth (usually switched off) defined one
# too. Keep this name unique to the role.
- name: Reboot after standard setup
ansible.builtin.reboot:
- name: Restart raspi-leds-off
ansible.builtin.service:

View File

@@ -61,7 +61,7 @@
when: pi_standard_setup_new_hostname | length > 0 and pi_standard_setup_pi_hostname.stdout != pi_standard_setup_new_hostname
register: pi_standard_setup_set_hostname
changed_when: true
notify: Reboot
notify: Reboot after standard setup
- name: Get hostname
ansible.builtin.command: "raspi-config nonint get_hostname"
register: pi_standard_setup_pi_hostname
@@ -115,6 +115,19 @@
- telnet
cache_valid_time: 7200
state: present
- name: Install the Pi 5 kernel
# Only where it can matter: a Pi 5 running an image that was built for an older Pi
# (a card moved over from a Pi 4). A current arm64 image already ships it, and then
# this is a no-op; an armhf userland needs the arm64 kernel package cross-installed.
ansible.builtin.apt:
name: "linux-image-rpi-2712:arm64"
cache_valid_time: 7200
state: present
when: >-
(pi_standard_setup_install_pi5_kernel | string | lower == 'true')
or (pi_standard_setup_install_pi5_kernel | string | lower == 'auto'
and 'Raspberry Pi 5' in (pi_standard_setup_raspberry_model.content | b64decode))
notify: Reboot after standard setup
- name: Copy vim config
ansible.builtin.copy:
src: vimrc

View File

@@ -4,6 +4,18 @@
name: python3-pip
cache_valid_time: 7200
state: present
- name: Apt install sysdweb's dbus and systemd dependencies
# sysdweb needs dbus-python ~= 1.4, which PyPI only ships as source (meson build needing
# pkg-config and the dbus/glib headers), and systemd-python, which needs libsystemd-dev.
# Trixie packages both (python3-dbus is exactly 1.4, python3-systemd 235), and pip counts
# the apt versions as satisfied. Bookworm has dbus 1.3, so there pip has to build 1.4.
ansible.builtin.apt:
name: >-
{{ ['python3-dbus', 'python3-systemd']
+ (['build-essential', 'pkg-config', 'python3-dev', 'libdbus-1-dev', 'libglib2.0-dev', 'libsystemd-dev']
if ansible_facts.distribution_major_version | int < 13 else []) }}
cache_valid_time: 7200
state: present
- name: Install sysdweb
ansible.builtin.pip:
name: sysdweb

View File

@@ -2,10 +2,12 @@
set -e # exit on error
TARGET_FOLDER="./rpi_image"
IMG_FILE_BASENAME="2023-12-11-raspios-bookworm-armhf-lite"
#DOWNLOAD_URL="https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-11-08/${IMG_FILE_BASENAME}.zip"
DOWNLOAD_URL="https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/${IMG_FILE_BASENAME}.img.xz"
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
TARGET_FOLDER="$SCRIPT_DIR/rpi_image"
#IMG_FILE_BASENAME="2023-12-11-raspios-bookworm-armhf-lite"
#DOWNLOAD_URL="https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/${IMG_FILE_BASENAME}.img.xz"
IMG_FILE_BASENAME="2026-09-15-raspios-trixie-arm64-lite"
DOWNLOAD_URL="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2026-09-15/${IMG_FILE_BASENAME}.img.xz"
echo "This script downloads raspian lite, and modifies the image to enable SSH and set hostname"
@@ -14,15 +16,20 @@ if [[ $EUID -ne 0 ]]; then
exit 1
fi
echo -n Hostname of new pi
read -p "Hostname of new pi [newrpi]: " RPI_HOSTNAME
read -p "WIFI SSID (only used if a password is given) [WLAN]: " WIFI_SSID
read -p "WIFI password (leave empty for non-wifi setup) [] " WIFI_PASSWORD
RPI_HOSTNAME=${RPI_HOSTNAME:-newrpi}
WIFI_SSID=${WIFI_SSID:-WLAN}
SCRIPT_DIR=`pwd`
mkdir -p $TARGET_FOLDER
cd $TARGET_FOLDER
mkdir -p "$TARGET_FOLDER"
cd "$TARGET_FOLDER"
if [ -e "${IMG_FILE_BASENAME}.img" ]; then
echo "${TARGET_FOLDER}/${IMG_FILE_BASENAME}.img already exists (already customised?)."
echo "Remove it first if you want to start over."
exit 1
fi
echo "Downloading image"
wget ${DOWNLOAD_URL}
@@ -36,36 +43,74 @@ unxz ${IMG_FILE_BASENAME}.img.xz
echo "Mounting image"
mkdir mounted_image
mkdir mounted_image/boot
mkdir mounted_image/system
losetup -P /dev/loop42 ${IMG_FILE_BASENAME}.img
mount /dev/loop42p1 mounted_image/boot
mount /dev/loop42p2 mounted_image/system
MOUNT_DIR=$(mktemp -d "$TARGET_FOLDER/mounted_image.XXXXXX")
LOOP_DEV=""
cleanup() {
set +e
umount "$MOUNT_DIR/boot" "$MOUNT_DIR/system" 2>/dev/null
[ -n "$LOOP_DEV" ] && losetup -d "$LOOP_DEV"
rmdir "$MOUNT_DIR/boot" "$MOUNT_DIR/system" "$MOUNT_DIR" 2>/dev/null
}
trap cleanup EXIT
mkdir "$MOUNT_DIR/boot" "$MOUNT_DIR/system"
LOOP_DEV=$(losetup -f --show -P ${IMG_FILE_BASENAME}.img)
mount ${LOOP_DEV}p1 "$MOUNT_DIR/boot" # firmware partition, /boot/firmware on the Pi
mount ${LOOP_DEV}p2 "$MOUNT_DIR/system"
echo "Enabling SSH and writing hostname"
echo $RPI_HOSTNAME > mounted_image/system/etc/hostname
touch mounted_image/boot/ssh # startup ssh
sed -i "/^#PermitRootLogin/ cPermitRootLogin prohibit-password" mounted_image/system/etc/ssh/sshd_config
mkdir -p mounted_image/system/root/.ssh
cat $SCRIPT_DIR/*.pub > mounted_image/system/root/.ssh/authorized_keys
chmod 700 mounted_image/system/root/.ssh
chmod 600 mounted_image/system/root/.ssh/authorized_keys
echo $RPI_HOSTNAME > "$MOUNT_DIR/system/etc/hostname"
touch "$MOUNT_DIR/boot/ssh" # startup ssh
sed -i "/^#PermitRootLogin/ cPermitRootLogin prohibit-password" "$MOUNT_DIR/system/etc/ssh/sshd_config"
mkdir -p "$MOUNT_DIR/system/root/.ssh"
cat $SCRIPT_DIR/*.pub > "$MOUNT_DIR/system/root/.ssh/authorized_keys"
chmod 700 "$MOUNT_DIR/system/root/.ssh"
chmod 600 "$MOUNT_DIR/system/root/.ssh/authorized_keys"
if [ -z "$WIFI_PASSWORD" ]
then
echo "Skipping WIFI setup"
elif [ -d "$MOUNT_DIR/system/etc/NetworkManager" ] && [ -e "$MOUNT_DIR/boot/user-data" ]
then
# Trixie (and bookworm images from 2023-10 on, when they use NetworkManager) ignore
# wpa_supplicant.conf on the boot partition; give NetworkManager a connection profile.
echo "Setting up initial wifi config (NetworkManager)"
NM_DIR="$MOUNT_DIR/system/etc/NetworkManager/system-connections"
mkdir -p "$NM_DIR"
/bin/cat <<EOM >"$NM_DIR/wifi.nmconnection"
[connection]
id=wifi
type=wifi
autoconnect=true
[wifi]
mode=infrastructure
ssid=${WIFI_SSID}
[wifi-security]
key-mgmt=wpa-psk
psk=${WIFI_PASSWORD}
[ipv4]
method=auto
[ipv6]
method=auto
EOM
chmod 600 "$NM_DIR/wifi.nmconnection" # NetworkManager ignores profiles readable by others
# regulatory domain, so the 5GHz channels and the right power limits are usable at boot
sed -i '1 s/$/ cfg80211.ieee80211_regdom=DE/' "$MOUNT_DIR/boot/cmdline.txt"
else
echo "Setting up initial wifi config"
/bin/cat <<EOM >mounted_image/boot/wpa_supplicant.conf
echo "Setting up initial wifi config (wpa_supplicant)"
/bin/cat <<EOM >"$MOUNT_DIR/boot/wpa_supplicant.conf"
country=DE
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="WLAN"
ssid="${WIFI_SSID}"
psk="${WIFI_PASSWORD}"
}
EOM
@@ -75,12 +120,8 @@ fi
echo "Unmounting image and other cleanup"
sleep 3 # to prevent "device is busy"
umount /dev/loop42p1
umount /dev/loop42p2
losetup -d /dev/loop42
rmdir mounted_image/boot
rmdir mounted_image/system
rmdir mounted_image
cleanup
trap - EXIT
rm ${IMG_FILE_BASENAME}.img.xz.sha256
chmod a+rw ${IMG_FILE_BASENAME}.img .