Chapter 3: Application Development
Modeled on the existing apps, write your first Luwu-OS app. This chapter gives you a minimal copy-paste template and explains theming, internationalization and registration, so you can get a new app onto the main UI quickly.
In this chapter:
| Section | What it covers | Difficulty |
|---|---|---|
| 3.1 PySide6 App Template | A minimal runnable app (template code) | ★★☆ |
| 3.2 Theme System | Color/typography/asset tokens and QSS helpers | ★★☆ |
| 3.3 Internationalization | Chinese/English switching (Translator / t / get_lang) | ★☆☆ |
| 3.4 Creating a New App | From creating the folder to registering it on the UI | ★★☆ |
| 3.5 Existing App Reference | Tech-stack overview of the official apps (dev examples) | ★☆☆ |
🧭 Reading path: For your first app, follow 3.1 → 3.4; to write it well, review the theming/i18n conventions in 3.2 / 3.3.
Prerequisites:
- Done Chapter 1, you can SSH into the robot;
- Read Chapter 2’s directory layout and IPC (know
apps/and/tmp/luwu_run.fifo); - Basic Python 3 and PySide6 (you can follow the template even if not fluent).
3.1 PySide6 App Template
Bottom line: writing an app in Luwu-OS means “a class inheriting AppFrame + a main()”. Why this shape? Because the launcher (Qt C++) owns the app lifecycle; your app only renders its own UI, and by inheriting AppFrame it gets the title bar, corner hints and key-to-exit common capabilities for free — you don't have to write the window shell yourself.
#!/usr/bin/env python3
import os, sys, time, signal
# Luwu-OS root
LUWU_ROOT = os.environ.get("LUWU_ROOT", "/opt/luwu-os")
if LUWU_ROOT not in sys.path:
sys.path.insert(0, LUWU_ROOT)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QKeyEvent
from PySide6.QtWidgets import QApplication, QWidget, QLabel
from libs.theme import apply_app_palette, Asset
from libs.ui import AppFrame
from libs.i18n import Translator
_T = Translator({
"cn": {"title": "我的应用", "corner_exit": "退出"},
"en": {"title": "My App", "corner_exit": "Exit"},
})
class MyPage(AppFrame):
def __init__(self):
super().__init__()
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.setTitle(_T("title"))
self.setCornerHints(
bl=(_T("corner_exit"), Asset.icon_back),
)
QTimer.singleShot(600000, self.close) # auto exit after 10 minutes
def keyPressEvent(self, ev: QKeyEvent):
if ev.key() == Qt.Key.Key_Back:
self.close()
def main():
signal.signal(signal.SIGINT, lambda *_: QApplication.instance().quit())
signal.signal(signal.SIGTERM, lambda *_: QApplication.instance().quit())
app = QApplication(sys.argv)
apply_app_palette(app)
w = MyPage()
w.showFullScreen()
sys.exit(app.exec())
if __name__ == "__main__":
main()
3.1.1 Run This Template
Save the template above as apps/my_new_app/main.py, then:
cd /opt/luwu-os/apps/my_new_app && python3 main.py # bare run: check logic (no theme/key routing)
# or let the launcher start it (recommended, verifies the real effect):
echo "apps/my_new_app/main.py" > /tmp/luwu_run.fifo
✅ Expected result: the physical screen shows “My App” fullscreen with an “Exit” corner hint in the title bar; press C key (Back) or the top-left back icon to exit; auto closes after 10 minutes of inactivity (
QTimer.singleShot(600000, ...)).⚠️ If a bare run fails with
ModuleNotFoundError: PySide6orNo module named 'libs', you're not running inside the Luwu-OS environment (must be on the robot, withLUWU_ROOT=/opt/luwu-os); if it fails withFailed to open /dev/fb-spi, it's usually a missing launcher context — start it via the fifo instead.
3.2 Theme System
Luwu-OS uses a light theme that is visually consistent with the launcher. Color / font / spacing / radius / asset constants are defined in libs/theme/tokens.py:
from libs.theme import Color, Font, Spacing, Radius, Asset, qss
# Color tokens (light theme)
Color.bg_solid # solid background #eaf0fb
Color.text_primary # primary text #1a3a6e
Color.text_secondary # secondary text #5d7299
Color.text_muted # muted text #8aa1c7
Color.text_invert # inverted text (on dark backgrounds) #ffffff
Color.accent # accent color #3a8dff
Color.success # success #18a957
Color.warning # warning #e69900
Color.danger # danger #d6453d
Color.card_bg # card background rgba(255,255,255,200)
# Font tokens (point sizes)
Font.family = "Noto Sans CJK SC"
Font.title / Font.subtitle / Font.body / Font.hint / Font.caption
# 18 15 14 12 11
# Asset tokens (located in launcher/assets/)
Asset.bg_image # main background bg_macos.png
Asset.icon_back # back icon
Asset.icon_enter # confirm/enter icon
Asset.icon_left # left arrow
Asset.icon_right # right arrow
# QSS helpers (libs/theme/qss.py, real function signatures)
qss.app_palette() # global base QSS for the app (font, scrollbar, ...)
qss.app_root() # app root style
qss.text(role="body", color=None) # text style (role: title/subtitle/body/hint/caption)
qss.card(selected=False) # card style
qss.chip(state="info") # status chip (info/success/warning/danger/muted)
qss.transparent() # transparent background
qss.overlay_pill(role="body", color=None, strong=False) # overlay pill (e.g. toast bar)
qss.corner_pill(color=None) # rounded pill (e.g. corner badge)
💡 Child apps should use these components and tokens directly and must not pick their own colors / font sizes or write raw
setStyleSheet(see the convention inlibs/ui/__init__.py).
Use a theme token for real (a styled label):
from PySide6.QtWidgets import QLabel
from libs.theme import qss, Color
label = QLabel("Notice")
label.setStyleSheet(qss.text(role="body", color=Color.warning))
This makes the text follow the theme's warning color instead of a hard-coded one; for rounded cards / status chips use qss.card() / qss.chip().
3.3 Internationalization
The single source of language config is /opt/luwu-os/configs/language.ini (a single line of plain text: cn or en).
from libs.i18n import Translator, t, get_lang, FONT_PATH
# Method 1: Translator dictionary object
_T = Translator({
"cn": {"hello": "你好"},
"en": {"hello": "Hello"},
})
label.setText(_T("hello"))
# Method 2: t() shortcut (single translation)
label.setText(t({"cn": "你好", "en": "Hello"}))
# Current language
lang = get_lang() # "cn" or "en"
Chinese font path FONT_PATH: prefers model/msyh.ttc, falls back to DroidSansFallbackFull.ttf / DejaVuSans.ttf (see _FONT_CANDIDATES in libs/i18n.py).
When does the language take effect? get_lang() reads configs/language.ini on every call, so edit that file and restart the app to switch language — no system reboot needed:
echo "en" > /opt/luwu-os/configs/language.ini # switch to English
echo "cn" > /opt/luwu-os/configs/language.ini # switch back to Chinese
💡 Official apps read the language once at startup in
main.pyand don't hot-switch at runtime; your custom apps can follow the same approach.
3.4 Creating a New App
mkdir -p /opt/luwu-os/apps/my_new_app- Create
main.py(use the template above) - Register it on the main screen: edit the
CARDSarray inlauncher/galleryview.cppand add one card entry (Chinese name, English name, card image, script path):
// launcher/galleryview.cpp
const CardData CARDS[CARD_COUNT] = {
{"无线网络", "WiFi", "card_network.png", "apps/network/main.py"},
{"图形化编程", "Coding", "card_coding.png", "apps/coding/main.py"},
{"AI交互", "AI Chat", "card_ai.png", "apps/ai_chat_pro/main.py"},
{"示例程序", "Demos", "card_more.png", "apps/demo_page/main.py"},
{"系统设置", "Settings", "card_settings.png", "apps/settings/main.py"},
// New: {"我的应用", "My App", "card_mine.png", "apps/my_new_app/main.py"},
};
If you want it to appear in the "Demos" grid instead, edit the demos array in launcher/demogridview.cpp (it supports model filter tags such as "@dog" / "!xgorider").
⚠️
launcher/devicetable.his the robot-model registry (used when adding a new XGO model); it has nothing to do with registering apps — do not modify it.
- Test it:
cd /opt/luwu-os/apps/my_new_app && python3 main.py # run directly
# or let the launcher start it (applies theme/key routing automatically):
echo "apps/my_new_app/main.py" > /tmp/luwu_run.fifo
⚠️ Common issues: - App doesn't appear on the main UI → check whether
CARDSingalleryview.cppadded a card (not replaced one); - Icon does not show → wrong card image path, or the image isn't inlauncher/assets/; - Tapping the card does nothing → the script path inCARDSshould beapps/xxx/main.py(relative toLUWU_ROOT), not an absolute path; - Want it in the “Demos” grid instead → editdemogridview.cpp, and note the model filter tags (@dog/!xgorider).
3.5 Reference: Existing Apps (Core Tech Stacks)
The apps under the official apps/ directory are good examples for secondary development. Their core technologies:
| App | Description | Core Technology |
|---|---|---|
ai_chat_pro / voice_chat | AI Chat Pro (new voice chat; reuses the wake-word / emotion modules in apps/ai) | Voice chat, wake-word, MQTT, cloud binding, expression animation |
ai | Old AI Chat Dev (dev AI; config page at http://<robot-ip>:5000) | LLM, TTS, ASR, ONNX emotion recognition |
coding | Blockly visual programming | Blockly, Python code generation |
face_follow | Face detection & tracking | MediaPipe Face Detection, Picamera2 |
ball_track | Color ball detection & tracking | OpenCV color filtering, PID control |
gesture | Gesture command control | MediaPipe Hands, ONNX |
gamepad | Bluetooth/USB gamepad control | evdev, Bluetooth HID, joystick calibration |
rc_mode | Web remote control | Flask web server, MJPEG streaming |
radar | 360° environment scanning | YDLiDAR SDK, real-time rendering |
group_perform | Multi-robot synchronization | MQTT pub/sub, time sync |
settings | System config & device info | Language switching, volume control |
📦 See the official application list in the luwu_os README for the full gallery view. Actual app processes render to
/dev/fb-spias needed and consistently use thelibs/uiandlibs/themecomponents, instead of writing their own rendering logic.
