Chapter 2 - System Architecture

Chapter 2: System Architecture

Before writing code on the robot, understand how Luwu-OS actually runs. This chapter explains its overall layering, hardware, boot order, inter-process communication and directory structure — so when you modify code you know what to change and what it affects.

In this chapter:

SectionWhat it coversDifficulty
2.1 Overall ArchitectureThree layers, 9 peripherals and “single-owner” hardware design (incl. 2.1.1 hardware spec table, 2.1.2 official four-layer model)★★☆
2.2 Boot FlowWhich systemd services run from power-on to UI ready★★☆
2.3 IPCFIFO, D-Bus, MQTT, and the debug entry to launch an app externally★★☆
2.4 Button SystemHow A/B/C/D keys map from the kernel to apps★★☆
2.5 Directory LayoutWhat each directory under /opt/luwu-os/ holds★☆☆
2.6 TroubleshootingBlack screen / app won't start / buttons unresponsive, etc.★☆☆

🧭 Reading path: To just tweak code, skip 2.2/2.4 and start with 2.5; to add features or apps, follow 2.1 → 2.3 → 2.5.

Prerequisites:

  • Done Chapter 1 and you can SSH into the robot;
  • Basic Linux commands (ls, cat, cd, etc.).

2.1 Overall Architecture

┌──────────────────────────────────────────────────────┐
│  User Layer                                            │
│  ┌────────────────────────────────────────────────┐  │
│  │     Qt 5.15 Launcher (C++)                      │  │
│  │  GalleryView │ StatusBar │ KeyFilter            │  │
│  └──────────────────┬─────────────────────────────┘  │
│                     │ FIFO pipes                       │
│  ┌──────────────────┴─────────────────────────────┐  │
│  │     PySide6 Applications (Python 3)             │  │
│  │  AI │ Coding │ Demos │ Settings │ ... 23 apps  │  │
│  └────────────────────────────────────────────────┘  │
├──────────────────────────────────────────────────────┤
│  System Layer (Linux Kernel)                          │
│  FBTFT │ gpio-keys │ ALSA dmix │ pwm-fan            │
│  Picamera2 │ BlueZ │ UART │ GPIO                     │
└──────────────────────────────────────────────────────┘

Core design principles: - The Qt C++ launcher manages the main UI and application lifecycle - Python PySide6 apps run as independent processes and communicate via FIFO pipes - No X11/Wayland — rendering goes directly to the Linux framebuffer (240×320 SPI LCD, mapped to /dev/fb-spi via a udev rule) - A crash in one app does not affect the others

2.1.1 Hardware Specifications

Luwu-OS consists of several peripherals, each with a single owner (a kernel driver or a system service); apps can only access hardware through standard interfaces:

#ComponentInterfaceDriver
1SPI LCD (ST7789V, 240×320)SPI0 + GPIO 27/25/0fbtft kernel driver → /dev/fb-spi
2Camera (OV5647)CSIPicamera2 (each app manages its own lifecycle)
34 Physical Buttons (A/B/C/D)GPIO 17/22/23/24gpio-keys/dev/input/eventX
4Bluetooth (Cypress)mini-UART / PL011bluetoothd via D-Bus
5Audio + Microphone (WM8960)I2S + I2CALSA dmix + dsnoop
6Robot Dog UARTttyAMA0 / UART5xgolib (auto-detect)
7Servo / IMU / BatteryUART → MCUxgolib forwarding
8Cooling Fan (4-wire PWM)GPIO PWM + TACHpwm-fan kernel driver (hwmon)
9Power + BatteryVC voltage monitor + UARTvcgencmd + xgolib joint monitoring

Design principle: Each hardware peripheral has exactly one owner (a kernel driver or system service); apps access hardware through standard interfaces, never by fighting over raw devices.

2.1.2 Architecture Layers (official)

LayerTech StackRole
LauncherQt 5.15 C++ (EGLFS / LinuxFB)Process lifecycle, key routing, status bar
AppsPySide6 Python (LinuxFB)Feature apps, rendered to framebuffer
Hardware AbstractionKernel drivers + systemd servicesSingle-owner hardware access via standard Linux interfaces
IPCFIFO (/tmp/luwu_keys.fifo), D-Bus, MQTTCross-process communication

2.2 Boot Flow

Power on → Kernel loads (gpio-keys/FBTFT/ALSA/PWM)
    → systemd starts
    → luwu-hw-autoconf (hardware detection; switches new/old hardware config.txt)
    → luwu-splash (boot splash screen)
    → luwu-launcher (main UI ready)
    → luwu-undervolt (undervoltage + battery monitoring)

Tip: The hardware model is probed at runtime by configs/detect_device.py (priority: live serial probe > manual override in configs/device.ini > unknown); the launcher uses this to filter out incompatible demos.

This process is chained by a few systemd services, each doing one clear job. Use systemctl to view and manage them:

systemd serviceRoleUseful command
luwu-splash.serviceBoot splash screensudo systemctl status luwu-splash
luwu-launcher.serviceMain UI (Qt launcher), the core onesudo systemctl restart luwu-launcher
luwu-hw-autoconf.serviceHardware auto-detect (CM4 / old-hardware config.txt switch)sudo systemctl status lwu-hw-autoconf
luwu-undervolt.serviceUndervoltage + battery monitoringsudo journalctl -u lwu-undervolt -f

💡 After changing code you normally only need to restart the corresponding app (see run.fifo in 2.3), not the whole machine; only kernel config / system-service changes need sudo reboot.

2.3 Inter-Process Communication (IPC)

FIFO PathDirectionPurpose
/tmp/luwu_keys.fifoLauncher → AppForwards key events (while a child app is running)
/tmp/luwu_preload.fifoLauncher → PreloadNotifies the preload process (apps/demo_page/preload_app.py)
/tmp/luwu_run.fifoExternal → LauncherAn external process writes a script path; the launcher starts it through its normal launchApp flow

At startup the launcher spawns apps/demo_page/preload_app.py (pre-loads heavy dependencies such as PySide6 to speed up the demos page) and notifies it via /tmp/luwu_preload.fifo; after a child app exits, the preload process is restarted as a fallback to repaint the screen and avoid blackouts.

/tmp/luwu_run.fifo is a debugging entry point to launch an app without going through the main menu:

# Path relative to LUWU_ROOT
echo "apps/foo/main.py" > /tmp/luwu_run.fifo
# Absolute path
echo "/opt/luwu-os/apps/foo/main.py" > /tmp/luwu_run.fifo

Other communication: MQTT (multi-robot group communication), UDP broadcast (group communication without internet).

How to debug a single app with run.fifo: this is the most common debugging path you'll use — have the launcher go through its full startup flow (apply theme, register key routing, attach the status bar) instead of a bare python3 main.py. The launcher then shows the app fullscreen on /dev/fb-spi, so you can see the real UI on the physical screen:

# ① Have the launcher start your app (a path relative to LUWU_ROOT is fine)
echo "apps/my_new_app/main.py" > /tmp/luwu_run.fifo

# ② Verify the launcher picked it up (see the recent log)
sudo journalctl -u luwu-launcher -n 50

💡 Bare python3 main.py vs. going through the fifo: a bare run won't apply the launcher's theme / key routing, and some apps may error or misrender without the launcher context. During development, bare-run first to check the logic, then use the fifo to verify the full effect.

2.4 Button System

Physical buttons are handled by the gpio-keys kernel driver:

Physical ButtonGPIOLinux KeyQt::Key
A (top-left)GPIO17KEY_LEFT (105)Qt::Key_Left
B (top-right)GPIO22KEY_RIGHT (106)Qt::Key_Right
C (bottom-left)GPIO23KEY_BACK (158)Qt::Key_Back
D (bottom-right)GPIO24KEY_ENTER (28)Qt::Key_Enter

The launcher's KeyFilter intercepts keys → writes to /tmp/luwu_keys.fifo → child apps read them via QSocketNotifier.

To handle keys inside an app, use keyPressEvent (PySide6 dispatches key events to the focused widget):

from PySide6.QtCore import Qt
from PySide6.QtGui import QKeyEvent

class MyPage(AppFrame):
    def keyPressEvent(self, ev: QKeyEvent):
        if ev.key() == Qt.Key.Key_Back:      # C key, bottom-left (val: 158)
            self.close()
        elif ev.key() == Qt.Key.Key_Enter:   # D key, bottom-right (val: 28)
            self.doSomething()
        else:
            super().keyPressEvent(ev)

⚠️ Use Qt.Key_Back / Qt.Key_Enter rather than hard-coded key values, to stay compatible with the key map of different models (see the physical-key → Qt-key table in 2.4).

2.5 Directory Layout

/opt/luwu-os/
├── apps/          # 23 apps (ai/ai_chat_pro/ball_catch/ball_track/
│                  #   bluetooth_gamepad/coding/demo_page/face_follow/gamepad/
│                  #   gesture/group_perform/hotspot/joystick/network/perform/
│                  #   person_follow/portal/radar/rc_mode/rl_demo/settings/
│                  #   sound_locate/voice_chat)
├── launcher/      # Qt5 C++ launcher (main.cpp/galleryview/demogridview/
│                  #   statusbar/keyfilter/devicetable/i18n)
├── configs/       # System config (install.sh/luwu-keys.dts/detect_device.py/
│                  #   hardware_autoconf.py/language.ini/systemd services/
│                  #   asound.conf/udev rules/luwu-undervolt-monitor.py)
├── libs/          # Shared libs (xgolib/xgoedu-luwuos/theme/ui/gamepad_config/
│                  #   ydlidar_sdk/i18n.py/ball_catch_core.py)
├── model/         # ONNX AI models (face/palm/hand/person/pose/emotion/
│                  #   gender-age/YOLO/wake-word(hi_luka, xiaolu_classmate etc.)/Chinese font msyh.ttc)
├── assets/        # Assets (expressions/images/music)
├── scripts/       # Utility scripts (check_update.py/edu_test.py/snap.py)
└── docs/          # Documentation

Notes on the layout:

  • In launcher/, the CARDS array in galleryview.cpp defines the 5 main-screen cards (WiFi / Coding / AI Chat / Demos / Settings); demogridview.cpp defines the 9 demos inside the "Demos" page; devicetable.h is the robot-model registry (add a new model by adding one line).
  • configs/language.ini is the global language setting (content is a single line: cn or en).
  • The mp_*.py files in model/ are Python wrapper classes for the MediaPipe ONNX models (MPPalmDet/MPHandPose/MPPersonDet/MPPose).

2.6 Troubleshooting

SymptomPossible causeFix
Main UI black after boot / no pictureSPI LCD not bound to /dev/fb-spiCheck ls -l /dev/fb-spi; inspect the config.txt and udev rule from install.sh steps 4–6
UI visible but wrong resolution / tearingFramebuffer size doesn't match the LCDCheck the SPI display parameters in /boot/firmware/config.txt
An app won't start; writing to run.fifo does nothingLauncher not running, or wrong pathConfirm systemctl status luwu-launcher; use a path relative to LUWU_ROOT; check the app log
Buttons unresponsiveKey device tree / udev not loadedls /dev/input/event*; confirm luwu-keys.dtbo is compiled & deployed (install.sh step 5)
Code changes don't take effectApp not restartedRestart the app via run.fifo (2.3); only reboot for system-service changes
Robot doesn't react to motion commandsSerial port busy / model not detectedSee Chapter 4, xgolib 4.1.9 common errors