Compare commits
14 Commits
8c225bbe06
...
84b0470b49
| Author | SHA1 | Date | |
|---|---|---|---|
| 84b0470b49 | |||
| 6423c134be | |||
| f2f50e98e1 | |||
| cb0ece0478 | |||
| e3722d2648 | |||
| a6d4180b2b | |||
| f6a169afaa | |||
| a0546f5fa6 | |||
| c3375bea75 | |||
| 6b9b50ef2e | |||
| 874a81f664 | |||
| f434429dd1 | |||
| ed7b14dcbb | |||
| 2fb290b146 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
2
roles/pi_hifiberry_amp/defaults/main.yml
Normal file
2
roles/pi_hifiberry_amp/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
hifiberry_powers_pi: false
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
6
roles/pi_irserver/defaults/main.yml
Normal file
6
roles/pi_irserver/defaults/main.yml
Normal 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
|
||||
@@ -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
|
||||
44
roles/pi_irserver/tasks/build.yml
Normal file
44
roles/pi_irserver/tasks/build.yml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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'.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
44
roles/pi_slint_frontend/README.md
Normal file
44
roles/pi_slint_frontend/README.md
Normal 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.
|
||||
42
roles/pi_slint_frontend/defaults/main.yml
Normal file
42
roles/pi_slint_frontend/defaults/main.yml
Normal 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: ""
|
||||
4
roles/pi_slint_frontend/handlers/main.yml
Normal file
4
roles/pi_slint_frontend/handlers/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
143
roles/pi_slint_frontend/tasks/main.yml
Normal file
143
roles/pi_slint_frontend/tasks/main.yml
Normal 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
|
||||
25
roles/pi_slint_frontend/templates/musikdelfin.service.j2
Normal file
25
roles/pi_slint_frontend/templates/musikdelfin.service.j2
Normal 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
|
||||
40
roles/pi_slint_frontend/templates/musikdelfin.sh.j2
Normal file
40
roles/pi_slint_frontend/templates/musikdelfin.sh.j2
Normal 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 }} "$@"
|
||||
@@ -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`.
|
||||
|
||||
34
roles/pi_squeezelite_custom/defaults/main.yml
Normal file
34
roles/pi_squeezelite_custom/defaults/main.yml
Normal 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
|
||||
41
roles/pi_squeezelite_custom/tasks/build.yml
Normal file
41
roles/pi_squeezelite_custom/tasks/build.yml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 .
|
||||
|
||||
|
||||
Reference in New Issue
Block a user