Collected fixes for the Raspberry Pi 5 + Touch Display 2 kiosk build. These are the recurring issues we’ve hit when imaging fresh installs; add more as needed.
Problem: Display shows a black vertical strip or half the screen is black after boot.
Solution: You must clear panning before setting framebuffer size and rotation:
DISPLAY=:0 xrandr --output DSI-2 --panning 0x0
DISPLAY=:0 xrandr --fb 720x1280
DISPLAY=:0 xrandr --output DSI-2 --mode 720x1280 --rotate rightProblem: Touch input doesn't align with where you tap on the screen.
Solution: Flip the kernel overlay flag in /boot/firmware/config.txt: change invx ↔ invy on the dtoverlay=vc4-kms-dsi-ili9881-7inch,... line. Reboot afterwards.
Problem: Trying to launch X server or Chromium from an SSH session fails.
Solution: startx over SSH won't work; Chromium kiosk needs a real TTY and a logged-in user. Stick with the console autologin path configured by setup.sh.
Problem: Applications fail with "Can't open display :0" error.
Solution: Display server isn't up yet or $DISPLAY isn't exported. Wait for the autologin session to start X, or explicitly export DISPLAY=:0 once X is running.
Problem: Chromium shows warnings about GCM (Google Cloud Messaging) or Vulkan in the logs.
Solution: Expected on minimal builds; harmless in kiosk mode and safe to ignore.
Problem: After running apt upgrade, the system no longer auto-logs in the pulse user.
Solution: Re-pin console autologin:
sudo raspi-config nonint do_boot_behaviour B2Problem: Need to identify which display connector is being used.
Solution: Run:
ls -1 /sys/class/drm | grep DSI # expect card0-DSI-2Problem: Bluetooth speaker won't connect, or it keeps turning off after periods of inactivity.
Solution: The bt-autoconnect.sh script automatically handles this:
-
Manual connection: Run the script manually to force a connection:
/home/pulse/bin/bt-autoconnect.sh
-
Check service status: Verify the autoconnect timer is running:
systemctl --user status bt-autoconnect.timer
-
Restart service: If the timer isn't running:
systemctl --user restart bt-autoconnect.timer systemctl --user enable --now bt-autoconnect.timer -
Speaker auto-power-off: Many Bluetooth speakers automatically power off after a period of inactivity. PulseOS includes a keepalive mechanism that sends a silent audio signal every 2 minutes to prevent this. The keepalive runs automatically when
PULSE_BLUETOOTH_AUTOCONNECT="true"is enabled. -
If speaker is off: Make sure the speaker is powered on. The autoconnect script will connect once the speaker is turned on and the script runs (every 15 seconds).
Problem: Wake-word beeps and pactl set-sink-volume feedback come through the speakers, but Music Assistant or Snapcast streams are silent. journalctl -u pulse-snapclient.service shows PulsePlayer connecting and then timing out (No chunk received for 5000ms, disconnecting from pulse.), and pactl list sink-inputs is empty while music is “playing”.
Solution: Snapclient uses the PulseAudio/PipeWire backend (--player pulse). If the pipewire-pulse user services aren’t running, Snapclient silently falls back to ALSA and no audio reaches the kiosk sink.
Heads-up about extra IPv6 errors: Installing the Debian/Ubuntu
snapclientpackage auto-enables the distro’s genericsnapclient.service, which immediately tries to connect to whatever host string it finds (often an IPv6 address you don’t use) and floods the logs with lines like:(Connection) Resolving host IP for: fd4b:9231:5453:fb5d:... (Connection) Connecting to [fd4b:...]:1704 (Connection) Failed to connect ..., error: Connection refusedThat noise isn’t our managed service—it’s the stock unit that ships with the package. Disable it once (or rerun
./setup.sh) so onlypulse-snapclient.servicestays active:sudo systemctl disable --now snapclient.service ./setup.sh <location> # optional, re-enables pulse-snapclient.serviceAfter that, only the Pulse-managed Snapclient will run.
- Rerun
./setup.sh <location>so the newconfigure_snapclient()path callsensure_user_systemd_session, which startspipewire.service,pipewire-pulse.service, andwireplumber.servicefor thepulseuser even when Bluetooth autoconnect is disabled. - Manual fix (if you can’t rerun setup right away):
sudo loginctl enable-linger pulse sudo -u pulse \ XDG_RUNTIME_DIR=/run/user/$(id -u pulse) \ DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u pulse)/bus \ systemctl --user enable --now pipewire.service pipewire-pulse.service wireplumber.service sudo systemctl restart pulse-snapclient.service
- Verify with
pactl list sink-inputs | grep -B3 snapclientwhile Music Assistant plays; you should now see asnapclientsink input and the speakers will output the stream.
Auto-recovery: A related failure looks identical from the couch but is not a PipeWire-not-running problem: snapclient resolves the
defaultsink once, when its player connects, so if that sink later disappears and is re-created with a new index — a USB DAC re-enumerating, or PipeWire restarting — snapclient keeps "connected" to the server while feeding a sink that no longer exists, and the room goes silent. Because the process never crashes,Restart=alwayscan't catch it.pulse-audio-watchdog.servicehandles this case: it asks the snapserver (overSNAPCAST_HTTP_PORT, default1780) whether this client should be playing and, if so but there is no localsnapclientsink input for ~20s, it restartspulse-snapclient.service. It never acts while the stream is idle, the client is muted/disconnected, or the server is unreachable. Watch it withjournalctl -u pulse-audio-watchdog.service -f. It is enabled automatically whenever the Snapcast client is enabled.
Problem: Music Assistant shows two players for the same Pulse (for example media_player.pulse_office and media_player.pulse_office_2), and the default Now Playing entity points at the unavailable one so the overlay/MQTT sensor never updates.
Solution: The duplication usually happens when Music Assistant’s Home Assistant MediaPlayers provider is enabled. That provider re-imports every HA media player— including the Snapcast player that Pulse already exposed—so MA creates a second entity and Home Assistant renames the duplicate with _2.
- Open Music Assistant → Settings → Player providers and disable (or delete) the Home Assistant MediaPlayers provider.
- In Home Assistant → Settings → Devices & Services → Music Assistant, remove any orphaned entities the provider created and keep only the Snapcast player for each kiosk.
- Rename the remaining entity back to
media_player.<hostname>if Home Assistant added_2. - If you want to keep multiple Music Assistant players (for multi-protocol hardware), set
PULSE_MEDIA_PLAYER_ENTITY="media_player.whichever_one_you_prefer"inpulse.conf. The kiosk overlay, Now Playing sensor, and alarm music playback will use that entity instead of the auto-detected one.
After either disabling the provider loop or overriding PULSE_MEDIA_PLAYER_ENTITY, the Now Playing sensor and pulse-photo-card will track the expected Music Assistant player again.
Problem: Display rotation is incorrect or glitchy after boot.
Solution: Confirm dtoverlay=vc4-kms-dsi-ili9881-7inch,rotation=90,dsi1,swapxy,invx still exists in /boot/firmware/config.txt. If missing or incorrect, rerun ./setup.sh to restore it.
Problem: Swapping to an external 10.1" HDMI display (instead of the ribbon DSI panel) needs the right resolution/rotation so the overlay fits and touch (if present) lines up.
Solution:
- Set the native mode in
/boot/config.txt(or/boot/firmware/config.txt). Example for 1280x800@60:
hdmi_force_hotplug=1
hdmi_group=2
hdmi_mode=28 # 1280x800@60
display_hdmi_rotate=0 # 0/1/2/3 = 0/90/180/270
Reboot after editing.
-
Ensure X uses the same mode. If EDID does not expose your mode, add an Xorg snippet
/etc/X11/xorg.conf.d/10-monitor.confwith a Modeline,PreferredMode, and optionalOption "Rotate" "left|right". -
Turn off power saving (HDMI panels usually have no GPIO backlight):
DISPLAY=:0 xset -dpms s off
If you have no backlight device, keep pulse-backlight disabled or set PULSE_BACKLIGHT_DEVICE in pulse.conf before running setup.sh so the generated /etc/pulse-backlight.conf points at a valid device.
-
Touch screens (if the HDMI panel is also USB touch): use
xinput listto find the device and apply aCoordinate Transformation Matrixin/etc/X11/xorg.conf.d/99-touch-calibration.confto match your rotation. -
Overlay sanity check: after boot, open the overlay and make sure cards are not clipped. Non-16:9/16:10 panels may need minor CSS padding tweaks, but the defaults work for 1280x800 and 1920x1080.
Problem: On Pi 5/CM5 with an HDMI panel, boot text/splash appears but X never starts (Xorg logs show modeset(G0): using drv /dev/dri/card1 then No devices detected).
Solution: Point X at the card that actually owns the HDMI connector (often card1 on Pi 5/CM5) and avoid the fbdev fallback.
- Remove the fbdev X driver (stops the framebuffer fallback that bails out):
sudo apt-get purge -y xserver-xorg-video-fbdev xserver-xorg-video-all
- Create
/etc/X11/xorg.confto force modesetting to the HDMI-capable card:
Section "ServerLayout"
Identifier "Layout0"
Screen 0 "Screen0" 0 0
EndSection
Section "Device"
Identifier "VC4"
Driver "modesetting"
Option "kmsdev" "/dev/dri/card1" # use the card that has HDMI-A-1 (see kmsprint)
Option "PrimaryGPU" "true"
EndSection
Section "Monitor"
Identifier "HDMI-1"
Option "PreferredMode" "1280x800"
EndSection
Section "Screen"
Identifier "Screen0"
Device "VC4"
Monitor "HDMI-1"
EndSection
If kmsprint shows HDMI-A-1 on card0, swap card1 to card0 above.
-
Ensure card permissions are normal (default is fine):
ls -l /dev/dri/card*should be groupvideoand mode660. -
Keep your HDMI kernel hints in
/boot/firmware/cmdline.txt, e.g.video=HDMI-A-1:1280x800M@60, and yourhdmi_force_hotplug/group/mode/rotatelines in/boot/firmware/config.txt.
After reboot, X should start and DISPLAY=:0 xrandr --listmonitors should list HDMI-1 at the expected mode.
Problem: Boot splash screen doesn't display correctly or shows errors.
Solution: Usually means the units plymouth-quit.service and plymouth-quit-wait.service lost their overrides. Rerun ./setup.sh to restore them.
Problem: The device keeps rebooting as soon as it finishes booting (watchdogs or the MQTT update button keep firing).
Solution: Automatic reboots route through /opt/pulse-os/bin/safe-reboot.sh, which enforces:
- A minimum uptime (
PULSE_REBOOT_MIN_UPTIME_SECONDS, default 300 s) before any auto-reboot is honored. - A rolling window limit (
PULSE_REBOOT_MAX_COUNTinsidePULSE_REBOOT_WINDOW_SECONDS, default 3 attempts per 900 s).
When the guard declines a reboot, you'll see pulse-safe-reboot entries in syslog explaining why it was skipped. Fix the underlying issue (bad kiosk URL, network outage, runaway MQTT automation), then either wait for the window to clear or reboot manually once you're ready.
If you prefer not to schedule a reboot at 03:00 every day, leave PULSE_DAILY_REBOOT_ENABLED="false" (the default) or set it explicitly to false and rerun ./setup.sh to disable the timer.
Problem: The device periodically loses Wi-Fi, becomes intermittently unreachable over SSH, or goes completely dark (no logs, no network) and only comes back after pulling power. Logs may show wpa_supplicant DISCONNECTED/ASSOC-REJECT churn, or simply stop entirely with no error.
Cause: The Pi's onboard brcmfmac Wi-Fi has two failure modes here:
- Power-save flapping — the radio sleeps between beacons, causing association churn and making the device intermittently unreachable.
- 5 GHz roam wedge — the SDIO firmware can wedge the whole network stack (and occasionally hang the board) when it roams onto a 5 GHz BSSID. The hang is silent — systemd stays alive but networking is dead, so nothing is logged.
Solution: setup.sh (configure_wifi) applies two generic mitigations automatically on every run:
- Installs
/etc/NetworkManager/conf.d/wifi-powersave-off.conf(wifi.powersave = 2) to disable power-save. - Pins the connection to 2.4 GHz (
band=bg), which is far more stable on these boards and removes the roam trigger.
Both are non-disruptive (they never bounce the Wi-Fi interface); the band pin takes effect on the next reboot. If a 2.4 GHz device still sees periodic multi-second stalls (snapcast time-sync timeouts every 60-90s with no disconnect), the cause is usually wpa_supplicant's full-spectrum roam scans — pin the device to its strongest AP with a bg <bssid> entry in config/wifi-band-policy (see the sample file), which disables roam scanning entirely. Verify with iw dev wlan0 get power_save (should read off) and iw dev wlan0 link (frequency should be 2.4 GHz, i.e. 2412–2472 MHz). On Raspberry Pi OS the interface is wlan0; if predictable names are enabled, substitute your interface (find it with iw dev).
As a backstop, the watchdog(8) daemon (see configure_watchdog) runs pulse-net-check.sh, which does a real TCP round-trip to an upstream host — not just a gateway ping the wedged firmware can still answer — so a silent wedge forces a hardware reset within a few minutes instead of waiting for a manual power-cycle.
Problem: The optional stock ticker bar (PULSE_TICKER_ENABLED=true) is missing, stuck
on stale numbers, or the logs show repeated ticker: yahoo .../ticker: stooq ... fetch
warnings.
Cause: The device fetches quotes directly from Yahoo Finance, so a DNS-level ad-blocker (Pi-hole, AdGuard Home, NextDNS) or a firewall can silently block the provider hosts. When Yahoo is unreachable, the fetcher falls back to its last-good cache, so the bar shows frozen values (or nothing on a fresh boot before the first successful fetch).
Solution: Allowlist the quote hosts on your DNS filter / firewall:
query1.finance.yahoo.com— v7 quote, crumb, and v8 chart endpointsfc.yahoo.com— cookie/crumb seed for the Yahoo quote APIfinnhub.io— only if you setPULSE_TICKER_API_KEY(licensed provider)
Confirm resolution from the device with nslookup query1.finance.yahoo.com (it should not
resolve to 0.0.0.0/your Pi-hole), then check the fetcher directly:
cd /opt/pulse-os && uv run python pulse/stock_ticker.py ^SPX,^DJI,^NDX. A healthy run prints
quote dicts; blocked hosts print the matching ticker: warning. Note the fast/slow poll
cadence is keyed to US market hours, so quotes only refresh every PULSE_TICKER_INTERVAL_CLOSED
seconds (default 15 min) outside US trading — a "stale" bar overnight can be normal.
Quotes come from Yahoo via two independent endpoints (v7 quote, then a no-auth v8 chart fallback) plus the cache. Setting
PULSE_TICKER_API_KEYadds Finnhub as a preferred, licensed source for the symbols it can price. Stooq was considered as a non-Yahoo source but now gates its CSV endpoints behind a JavaScript anti-bot challenge, so it is unusable from a headless client.
Send PRs with any other gotchas so future builders don't have to rediscover them.