Skip to content

Single‑App Fullscreen UI (No Window Manager)

Time estimate: ~30 minutes Prerequisites: Framebuffer Basics

Learning Objectives

By the end of this tutorial you will be able to:

  • Render a status UI as an image and push it to the framebuffer
  • Build a live-updating display loop without a window manager
  • Explain the render-loop pattern used in embedded single-app UIs
Kiosk Mode and Graphics Stack Choice

Many embedded products — industrial panels, medical devices, vehicle dashboards — run a single fullscreen application with no window manager. This "kiosk mode" pattern eliminates the X11/Wayland compositor stack entirely, reducing boot time, RAM usage, and attack surface. The application chooses a graphics path based on its needs: direct framebuffer writes (/dev/fb0) for simple image-based UIs, SDL2 with kmsdrm for 2D/3D rendering with VSync, or Qt with EGLFS for declarative UIs. Each path trades complexity for capability — framebuffer writes need no dependencies but offer no GPU acceleration; EGLFS gives full OpenGL ES but requires Qt's runtime (~40 MB).

See also: Graphics Stack reference


Introduction

Many embedded products — industrial panels, kiosks, medical devices, vehicle dashboards — run a single fullscreen application. They don't need window management, task switching, or a desktop environment.

The pattern is simple:

  1. Generate a UI image in memory (using PIL, OpenCV, or any library)
  2. Push it to the display via framebuffer (fbi or direct /dev/fb0 writes)
  3. Loop — update data, regenerate image, push again

This avoids the complexity of X11/Wayland and reduces boot time significantly. The trade-off is that you must handle all rendering yourself — there are no widgets or layout managers.


1. Install Python Tools

Concept: A small rendering stack is enough for a single‑screen UI.

sudo apt-get install -y python3-pil

2. Render a Simple UI

Concept: You are generating the UI as an image, then pushing it to the display.

python3 - <<'PY'
from PIL import Image, ImageDraw, ImageFont

W, H = 800, 480
img = Image.new("RGB", (W, H), (10, 10, 10))
draw = ImageDraw.Draw(img)

draw.rectangle([20, 20, W-20, 120], outline=(0,200,255), width=3)
draw.text((40, 40), "SYSTEM STATUS", fill=(255,255,255))

draw.text((40, 160), "Temp: 42 C", fill=(0,255,0))
draw.text((40, 220), "CPU: 18 %", fill=(0,255,0))
draw.text((40, 280), "NET: OK", fill=(0,255,0))

img.save("ui.png")
PY
Checkpoint

After running the script, display ui.png on the framebuffer:

sudo fbi -T 1 -d /dev/fb0 ui.png
You should see a dark screen with "SYSTEM STATUS" and three status lines.

Stuck?
  • "fbi: command not found" — install with sudo apt install -y fbi
  • Blank screen — verify framebuffer exists: ls /dev/fb0
  • Wrong size — adjust W, H values to match your display resolution (check with fbset -i)

3. Show on Framebuffer

Concept: This bypasses X11/Wayland and reduces boot time and complexity.

sudo fbi -T 1 -d /dev/fb0 ui.png

4. Make It Live (Simple Loop)

Concept: A loop + redraw is the simplest embedded UI architecture.

python3 - <<'PY'
import time, os, psutil
from PIL import Image, ImageDraw

W, H = 800, 480

while True:
    img = Image.new("RGB", (W, H), (10, 10, 10))
    d = ImageDraw.Draw(img)
    temp = "N/A"
    try:
        with open("/sys/class/thermal/thermal_zone0/temp") as f:
            temp = f\"{int(f.read())/1000:.1f} C\"
    except Exception:
        pass
    cpu = psutil.cpu_percent(interval=0.1)

    d.text((40, 40), "SYSTEM STATUS", fill=(255,255,255))
    d.text((40, 120), f"Temp: {temp}", fill=(0,255,0))
    d.text((40, 160), f"CPU: {cpu:.1f} %", fill=(0,255,0))
    img.save("ui.png")
    os.system("sudo fbi -T 1 -d /dev/fb0 -noverbose ui.png")
    time.sleep(1)
PY
Stuck?
  • "psutil not found" — install with sudo apt install -y python3-psutil or pip3 install psutil
  • Display flickers — this is normal with fbi in a loop; for production, write directly to /dev/fb0 instead

What Just Happened?

You built a single-app embedded UI using the simplest possible display stack:

Layer Desktop Linux Your Embedded UI
Window manager X11/Wayland None
Toolkit Qt/GTK PIL (image generation)
Display Compositor Direct framebuffer
Process model Multiple windows Single fullscreen app

This architecture boots faster, uses less memory, and has fewer failure points. It's the standard approach for embedded products with a single-purpose display.


Challenges

Challenge 1: Add Network Status

Add a line showing network status (IP address and link state):

import subprocess
ip = subprocess.check_output(["hostname", "-I"]).decode().strip()
draw.text((40, 340), f"IP: {ip}", fill=(0,255,0))

Challenge 2: Color Thresholds

Change the temperature text color based on value: green below 50°C, yellow 50-70°C, red above 70°C.

Challenge 3: Add Timestamp

Display the current date and time on the UI, updating each loop iteration.

Deliverable

Screenshot or photo of the UI running on the display, showing live-updating system data.


Course Overview | Next: Data Logger →