Skip to content

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 fbi or 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:

cd ~
git clone https://github.com/OE-KVK-H2IoT/embedded-linux.git

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:

sudo cp ~/embedded-linux/services/*.service /etc/systemd/system/
sudo systemctl daemon-reload

Open one to understand its structure:

cat /etc/systemd/system/level-sdl2.service
[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 nano /etc/systemd/system/level-sdl2.service
After editing, always reload: sudo systemctl daemon-reload

Checkpoint

Verify all three services are recognized:

systemctl list-unit-files | grep -E "level-sdl2|sdl2-dashboard|level-display-py"
All three should appear with state disabled.


2. Enable and Test

Enable and start the level display service:

sudo systemctl enable level-sdl2.service
sudo systemctl start level-sdl2.service

Check that it is running:

systemctl status level-sdl2.service

You should see Active: active (running). The display should show the level bubble.

Now test the Conflicts= directive — start the dashboard instead:

sudo systemctl start sdl2-dashboard.service

Check both services:

systemctl is-active level-sdl2.service
systemctl is-active sdl2-dashboard.service

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?

sudo reboot

After the Pi comes back up, SSH in and verify:

systemctl is-active level-sdl2.service

It should be active — the service you enabled starts automatically at boot.

Checkpoint
  • level-sdl2.service is running after reboot
  • Starting sdl2-dashboard.service automatically stopped level-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)

sudo sh -c 'echo 0 > /sys/class/vtconsole/vtcon1/bind'

This immediately releases the framebuffer from the text console. To make it persistent, add it to your service file:

ExecStartPre=/bin/sh -c 'echo 0 > /sys/class/vtconsole/vtcon1/bind'

b) Mask getty on tty1

The getty@tty1 service shows the login prompt. Mask it so it never starts:

sudo systemctl mask getty@tty1.service
Undo with unmask

sudo systemctl unmask getty@tty1.service
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:

console=ttyS0,115200 root=PARTUUID=... rootfstype=ext4 ... 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:

sudo kiosk-select

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:

sudo systemctl disable --now level-sdl2.service
sudo systemctl enable --now sdl2-dashboard.service
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

sudo apt 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:

sudo systemctl enable splash.service

Add splash to the kernel command line

Edit /boot/firmware/cmdline.txt and add splash (alongside quiet):

console=ttyS0,115200 ... quiet splash
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:

ls /dev/fb0
If it is missing, use Option B (Plymouth) or Challenge 4 (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

sudo apt install plymouth plymouth-themes

Pick a theme

List available themes and select one:

plymouth-set-default-theme --list
sudo plymouth-set-default-theme spinner

Update the initramfs

Plymouth must be baked into the initramfs to run early enough:

sudo update-initramfs -u

Kernel command line

Ensure quiet splash is in /boot/firmware/cmdline.txt (same as Option A).

Custom theme (optional)

Create a custom theme directory:

sudo mkdir -p /usr/share/plymouth/themes/my-kiosk

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:

sudo plymouth-set-default-theme my-kiosk
sudo update-initramfs -u
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:

After=multi-user.target plymouth-quit.service
Or force Plymouth to quit before your app starts:
ExecStartPre=/usr/bin/plymouth quit

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:

screen /dev/ttyUSB0 115200
# or
minicom -D /dev/ttyUSB0 -b 115200
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):

  1. Power off the Pi and remove the SD card
  2. Insert it into your host machine
  3. Mount the boot partition and fix cmdline.txt
  4. Mount the rootfs partition and fix service files:
    # Remove a broken service
    sudo rm /mnt/rootfs/etc/systemd/system/broken.service
    # Or unmask getty to get the console back
    sudo rm /mnt/rootfs/etc/systemd/system/getty@tty1.service
    
  5. 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:

WatchdogSec=30
In your C code, periodically notify systemd that the app is alive:
#include <systemd/sd-daemon.h>
sd_notify(0, "WATCHDOG=1");
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 --vo=drm --loop=inf /home/pi/splash.mp4
Create a service that starts 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 status screenshot)
  • [ ] 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

Course Overview | Next: Boot Timing →