Kiosk Service: Auto‑Start, Console Management & Boot Splash
Time estimate: ~60 minutes Prerequisites: Level Display: SDL2, SDL2 Dashboard, Data Logger Appliance
Learning Objectives
By the end of this tutorial you will be able to:
- Write a systemd service unit that auto-starts a display application at boot
- Use
Conflicts=to ensure only one display app runs at a time - Suppress the Linux text console so graphics fill the screen
- Switch between kiosk apps using a helper script
- Add a boot splash screen using
fbior Plymouth - Debug a headless kiosk using journal logs, UART serial, and boot analysis tools
From Development to Production
Running a display app manually over SSH (sudo SDL_VIDEODRIVER=kmsdrm ./level_sdl2) works during development, but a production device must:
- Start the app automatically when powered on — no SSH, no keyboard
- Restart on crash — an unattended display cannot stay blank
- Hide the text console — users should see graphics, not a login prompt
- Show a splash screen — a branded image while the system boots, not a wall of kernel messages
This is the kiosk pattern: a single-purpose device that boots directly into a fullscreen application. Vending machines, digital signage, dashboards, and point-of-sale terminals all use this pattern. On Linux, the combination of systemd (auto-start + restart), console suppression (vtconsole + getty masking), and a boot splash (fbi or Plymouth) implements it.
Course Source Repository
This tutorial references source files from the course repository. If you haven't cloned it yet on your Pi:
Source files for this tutorial are in ~/embedded-linux/services/.
1. Install the Service Files
Three pre-written service files are provided in src/embedded-linux/services/. Each one runs a different display application as a kiosk.
Copy them to the Pi:
Open one to understand its structure:
[Unit]
Description=Level Display (SDL2 + DRM/KMS)
After=multi-user.target
Conflicts=sdl2-dashboard.service level-display-py.service
[Service]
Type=simple
ExecStart=/home/pi/embedded-linux/apps/level-display/level_sdl2
Environment=SDL_VIDEODRIVER=kmsdrm
Restart=on-failure
RestartSec=3
User=root
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Anatomy of a Systemd Service Unit
A .service file has three sections:
[Unit] — identity and ordering
| Directive | Purpose |
|---|---|
Description= |
Human-readable name shown in systemctl status |
After= |
Start this service only after the listed targets/services are ready |
Conflicts= |
If this service starts, systemd automatically stops the conflicting ones |
[Service] — how to run it
| Directive | Purpose |
|---|---|
Type=simple |
systemd considers the service started as soon as ExecStart forks |
ExecStart= |
The command to run — must be an absolute path |
Environment= |
Set environment variables (here: tell SDL2 to use DRM/KMS, not X11) |
Restart=on-failure |
Restart automatically if the process exits with a non-zero status |
RestartSec=3 |
Wait 3 seconds before restarting (prevents crash loops from consuming resources) |
User=root |
Run as root — required for direct DRM/KMS and GPIO/SPI access |
StandardOutput=journal |
Send stdout to the systemd journal (viewable with journalctl) |
[Install] — when to start
| Directive | Purpose |
|---|---|
WantedBy=multi-user.target |
Enable this service for the normal multi-user boot (equivalent to the old "runlevel 3") |
Warning
The ExecStart= paths assume your apps are built under /home/pi/embedded-linux/apps/. If your build directory is different, edit the .service files before installing:
sudo systemctl daemon-reload
Checkpoint
Verify all three services are recognized:
All three should appear with statedisabled.
2. Enable and Test
Enable and start the level display service:
Check that it is running:
You should see Active: active (running). The display should show the level bubble.
Now test the Conflicts= directive — start the dashboard instead:
Check both services:
The level display should now be inactive (killed automatically by Conflicts=) and the dashboard active.
Reboot test
The ultimate test — does it survive a reboot?
After the Pi comes back up, SSH in and verify:
It should be active — the service you enabled starts automatically at boot.
Checkpoint
level-sdl2.serviceis running after reboot- Starting
sdl2-dashboard.serviceautomatically stoppedlevel-sdl2.service
3. Suppress the Text Console
Even with a kiosk service running, the Linux text console (tty1) may flicker underneath or appear briefly during boot. Three layers of suppression fix this.
a) Unbind the framebuffer console (runtime)
This immediately releases the framebuffer from the text console. To make it persistent, add it to your service file:
b) Mask getty on tty1
The getty@tty1 service shows the login prompt. Mask it so it never starts:
Undo with unmask
Use this if you need the text console back for debugging.
c) Redirect kernel messages to serial
Edit /boot/firmware/cmdline.txt (or /boot/cmdline.txt on older Raspberry Pi OS):
Remove console=tty1 and add console=ttyS0,115200 quiet:
Warning
UART name varies by Pi model:
| Model | UART device |
|---|---|
| Pi 3 | ttyAMA0 |
| Pi 4 / Pi 5 | ttyS0 |
Using the wrong name means no serial console output. Check your model with cat /proc/device-tree/model.
The quiet flag suppresses most kernel boot messages. Combined with the serial redirect, the display stays clean from power-on.
Checkpoint
After reboot: - No login prompt on the display - No kernel messages on the display - Kiosk app starts fullscreen without console flicker
4. Switch Between Apps
Switching apps requires disabling the current service and enabling a new one. The kiosk-select helper script automates this.
Install the script
sudo cp ~/embedded-linux/scripts/kiosk-service/kiosk-select /usr/local/bin/
sudo chmod +x /usr/local/bin/kiosk-select
Usage
sudo kiosk-select level-sdl2 # switch to level display
sudo kiosk-select sdl2-dashboard # switch to dashboard
sudo kiosk-select level-display-py # switch to Python version
sudo kiosk-select none # disable all kiosk services
Run without arguments to see the current state:
Output:
Usage: sudo kiosk-select <app>
Apps:
level-sdl2 Level display (SDL2 + DRM/KMS)
sdl2-dashboard Multi-gauge dashboard (SDL2 + DRM/KMS)
level-display-py Level display (Python + framebuffer)
none Disable all display services
Current state:
level-sdl2 enabled=enabled active=active
sdl2-dashboard enabled=disabled active=inactive
level-display-py enabled=disabled active=inactive
Manual alternative
If you prefer not to use the script, the equivalent manual steps are:
Checkpoint
Use kiosk-select to switch between all three apps. Verify each one displays correctly before switching to the next.
5. Boot Splash — Option A (fbi)
fbi (framebuffer imageviewer) displays a static image on /dev/fb0 before the kiosk app takes over. This is the simplest splash approach.
Install fbi
Create a splash service
Create /etc/systemd/system/splash.service:
[Unit]
Description=Boot Splash Screen
DefaultDependencies=no
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/bin/fbi --noverbose --autozoom /home/pi/splash.png
StandardInput=tty
TTYPath=/dev/tty1
[Install]
WantedBy=sysinit.target
Place your splash image (a PNG matching your display resolution) at /home/pi/splash.png.
Enable it:
Add splash to the kernel command line
Edit /boot/firmware/cmdline.txt and add splash (alongside quiet):
Warning
/dev/fb0 may not exist on Pi 4/5 with full KMS enabled. In that case, fbi will fail because it needs a legacy framebuffer device. Check with:
mpv --vo=drm) instead.
Checkpoint
Reboot. You should see your splash image for a few seconds before the kiosk app takes over.
6. Boot Splash — Option B (Plymouth)
Plymouth is a more capable splash system that supports animations, progress bars, and smooth transitions. It runs early in the boot process using DRM directly — no /dev/fb0 needed.
Install Plymouth
Pick a theme
List available themes and select one:
Update the initramfs
Plymouth must be baked into the initramfs to run early enough:
Kernel command line
Ensure quiet splash is in /boot/firmware/cmdline.txt (same as Option A).
Custom theme (optional)
Create a custom theme directory:
Create /usr/share/plymouth/themes/my-kiosk/my-kiosk.plymouth:
[Plymouth Theme]
Name=My Kiosk
Description=Custom kiosk boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/my-kiosk
ScriptFile=/usr/share/plymouth/themes/my-kiosk/my-kiosk.script
Create /usr/share/plymouth/themes/my-kiosk/my-kiosk.script:
logo = Image("logo.png");
logo_sprite = Sprite(logo);
logo_sprite.SetX(Window.GetWidth() / 2 - logo.GetWidth() / 2);
logo_sprite.SetY(Window.GetHeight() / 2 - logo.GetHeight() / 2);
logo_sprite.SetOpacity(1);
Place your logo.png in the theme directory, then activate:
Warning
Plymouth holds /dev/dri/card0 during the splash. Your kiosk service cannot open DRM until Plymouth releases it. Plymouth quits automatically when the boot target is reached, but if your service starts too early, add this to the [Unit] section:
Checkpoint
Reboot. The Plymouth splash should appear during early boot, then transition smoothly to your kiosk app.
7. Debugging a Headless Kiosk
Once the text console is suppressed, you cannot see error messages on the display. These tools let you diagnose problems remotely.
Journal logs
The most important tool — all service output goes to the journal:
# Live log stream (like tail -f)
journalctl -u level-sdl2.service -f
# Last 50 lines
journalctl -u level-sdl2.service -n 50
# Logs from current boot only
journalctl -u level-sdl2.service -b
UART serial console
If SSH is unavailable (network misconfigured, system hanging during boot), connect a USB-to-UART adapter to the Pi's GPIO header:
| Pi pin | UART adapter |
|---|---|
| GND (pin 6) | GND |
| TX (GPIO 14, pin 8) | RX |
| RX (GPIO 15, pin 10) | TX |
On your host machine:
Kernel messages on serial
If you followed Section 3c (console=ttyS0,115200), all kernel messages appear here — including boot failures that happen before SSH is available.
Boot analysis
Identify which services are slowing down boot:
# Time spent in each boot phase
systemd-analyze
# Blame list: services sorted by startup time
systemd-analyze blame
# Critical path: the chain of dependencies that determines total boot time
systemd-analyze critical-chain level-sdl2.service
Emergency SD card edit
If the Pi does not boot at all (bad cmdline.txt, broken service causing boot loop):
- Power off the Pi and remove the SD card
- Insert it into your host machine
- Mount the boot partition and fix
cmdline.txt - Mount the rootfs partition and fix service files:
- Reinsert the SD card and boot
Checkpoint
- You can read kiosk service logs with
journalctl - You know how to connect a UART serial console
- You can identify the slowest boot services with
systemd-analyze blame
What Just Happened?
You turned a manually-launched display app into a production kiosk appliance:
graph TD
A[Power On] --> B[Bootloader]
B --> C[Kernel + quiet]
C --> D{Splash configured?}
D -->|Option A| E[fbi shows PNG on /dev/fb0]
D -->|Option B| F[Plymouth shows splash on DRM]
D -->|Neither| G[Blank screen]
E --> H[systemd starts kiosk service]
F --> H
G --> H
H --> I[Kiosk app takes over display]
I --> J{App crashes?}
J -->|Yes| K[systemd restarts after 3s]
K --> I
J -->|No| L[Running until power off]
The key components:
- systemd service — auto-start, crash recovery,
Conflicts=for mutual exclusion - Console suppression — vtconsole unbind, getty mask, serial redirect
- Boot splash — branded image or animation from power-on to app-ready
kiosk-select— switch between apps without remembering systemctl commands- Remote debugging — journal, serial, boot analysis, SD card rescue
This is the same architecture used in commercial kiosk products, digital signage, and embedded HMI panels.
Challenges
Challenge 1: Watchdog Integration
Add a hardware watchdog to your kiosk service. In the service file:
In your C code, periodically notify systemd that the app is alive: Link with-lsystemd. If the app hangs (stops calling sd_notify), systemd will kill and restart it after 30 seconds.
Challenge 2: Boot Time Budget
Measure the time from power-on to first frame displayed. Use systemd-analyze and dmesg timestamps. Target: under 10 seconds. Techniques:
- Disable unused services (bluetooth, avahi-daemon, triggerhappy)
- Use systemd-analyze critical-chain to find the bottleneck
- See the Boot Timing tutorial for a systematic approach
Challenge 3: Dynamic App Selection
Modify kiosk-select to read a config file (/etc/kiosk.conf) that specifies which app to run. Create a oneshot service that reads the config at boot and enables the correct kiosk service. This way, a field technician can change the app by editing a single file.
Challenge 4: Video Boot Splash
Replace the static image with a looping video using mpv:
mpv early and kills it when the kiosk service is ready (use ExecStartPre=/usr/bin/pkill mpv in the kiosk service).
Deliverable
- [ ] Kiosk service running and auto-starting after reboot (
systemctl statusscreenshot) - [ ] Text console fully suppressed (no login prompt, no kernel messages on display)
- [ ] Successful switch between at least two apps using
kiosk-select - [ ] Boot splash visible during startup (Option A or B)
- [ ] Journal log output showing service start and at least one restart cycle