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:
- Generate a UI image in memory (using PIL, OpenCV, or any library)
- Push it to the display via framebuffer (
fbior direct/dev/fb0writes) - 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.
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:
Stuck?
- "fbi: command not found" — install with
sudo apt install -y fbi - Blank screen — verify framebuffer exists:
ls /dev/fb0 - Wrong size — adjust
W, Hvalues to match your display resolution (check withfbset -i)
3. Show on Framebuffer
Concept: This bypasses X11/Wayland and reduces boot time and complexity.
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-psutilorpip3 install psutil - Display flickers — this is normal with
fbiin a loop; for production, write directly to/dev/fb0instead
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):
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.