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:
| Section | What it covers | Difficulty |
|---|---|---|
| 2.1 Overall Architecture | Three 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 Flow | Which systemd services run from power-on to UI ready | ★★☆ |
| 2.3 IPC | FIFO, D-Bus, MQTT, and the debug entry to launch an app externally | ★★☆ |
| 2.4 Button System | How A/B/C/D keys map from the kernel to apps | ★★☆ |
| 2.5 Directory Layout | What each directory under /opt/luwu-os/ holds | ★☆☆ |
| 2.6 Troubleshooting | Black 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:
| # | Component | Interface | Driver |
|---|---|---|---|
| 1 | SPI LCD (ST7789V, 240×320) | SPI0 + GPIO 27/25/0 | fbtft kernel driver → /dev/fb-spi |
| 2 | Camera (OV5647) | CSI | Picamera2 (each app manages its own lifecycle) |
| 3 | 4 Physical Buttons (A/B/C/D) | GPIO 17/22/23/24 | gpio-keys → /dev/input/eventX |
| 4 | Bluetooth (Cypress) | mini-UART / PL011 | bluetoothd via D-Bus |
| 5 | Audio + Microphone (WM8960) | I2S + I2C | ALSA dmix + dsnoop |
| 6 | Robot Dog UART | ttyAMA0 / UART5 | xgolib (auto-detect) |
| 7 | Servo / IMU / Battery | UART → MCU | xgolib forwarding |
| 8 | Cooling Fan (4-wire PWM) | GPIO PWM + TACH | pwm-fan kernel driver (hwmon) |
| 9 | Power + Battery | VC voltage monitor + UART | vcgencmd + 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)
| Layer | Tech Stack | Role |
|---|---|---|
| Launcher | Qt 5.15 C++ (EGLFS / LinuxFB) | Process lifecycle, key routing, status bar |
| Apps | PySide6 Python (LinuxFB) | Feature apps, rendered to framebuffer |
| Hardware Abstraction | Kernel drivers + systemd services | Single-owner hardware access via standard Linux interfaces |
| IPC | FIFO (/tmp/luwu_keys.fifo), D-Bus, MQTT | Cross-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 inconfigs/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 service | Role | Useful command |
|---|---|---|
luwu-splash.service | Boot splash screen | sudo systemctl status luwu-splash |
luwu-launcher.service | Main UI (Qt launcher), the core one | sudo systemctl restart luwu-launcher |
luwu-hw-autoconf.service | Hardware auto-detect (CM4 / old-hardware config.txt switch) | sudo systemctl status lwu-hw-autoconf |
luwu-undervolt.service | Undervoltage + battery monitoring | sudo 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 Path | Direction | Purpose |
|---|---|---|
/tmp/luwu_keys.fifo | Launcher → App | Forwards key events (while a child app is running) |
/tmp/luwu_preload.fifo | Launcher → Preload | Notifies the preload process (apps/demo_page/preload_app.py) |
/tmp/luwu_run.fifo | External → Launcher | An 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.pyvs. 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 Button | GPIO | Linux Key | Qt::Key |
|---|---|---|---|
| A (top-left) | GPIO17 | KEY_LEFT (105) | Qt::Key_Left |
| B (top-right) | GPIO22 | KEY_RIGHT (106) | Qt::Key_Right |
| C (bottom-left) | GPIO23 | KEY_BACK (158) | Qt::Key_Back |
| D (bottom-right) | GPIO24 | KEY_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_Enterrather 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/, theCARDSarray ingalleryview.cppdefines the 5 main-screen cards (WiFi / Coding / AI Chat / Demos / Settings);demogridview.cppdefines the 9 demos inside the "Demos" page;devicetable.his the robot-model registry (add a new model by adding one line). configs/language.iniis the global language setting (content is a single line:cnoren).- The
mp_*.pyfiles inmodel/are Python wrapper classes for the MediaPipe ONNX models (MPPalmDet/MPHandPose/MPPersonDet/MPPose).
2.6 Troubleshooting
| Symptom | Possible cause | Fix |
|---|---|---|
| Main UI black after boot / no picture | SPI LCD not bound to /dev/fb-spi | Check ls -l /dev/fb-spi; inspect the config.txt and udev rule from install.sh steps 4–6 |
| UI visible but wrong resolution / tearing | Framebuffer size doesn't match the LCD | Check the SPI display parameters in /boot/firmware/config.txt |
An app won't start; writing to run.fifo does nothing | Launcher not running, or wrong path | Confirm systemctl status luwu-launcher; use a path relative to LUWU_ROOT; check the app log |
| Buttons unresponsive | Key device tree / udev not loaded | ls /dev/input/event*; confirm luwu-keys.dtbo is compiled & deployed (install.sh step 5) |
| Code changes don't take effect | App not restarted | Restart the app via run.fifo (2.3); only reboot for system-service changes |
| Robot doesn't react to motion commands | Serial port busy / model not detected | See Chapter 4, xgolib 4.1.9 common errors |
