Files
MoreTore/UICheatSheet.md
T
firestar5683 3d8af2361e Rename
2026-03-27 18:05:44 -05:00

2973 lines
128 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# StarPilot UI Architecture: Qt to Raylib Porting Cheat Sheet
This document serves as the definitive reference for porting the Qt-based Big UI to the Raylib-based Big UI in StarPilot. It provides direct parallels, code patterns, and implementation guidance.
> **Last updated: March 17, 2026 (Late Session)** — **Fully implemented Driving Model panel** with High-Fidelity Qt SelectionDialog (Favorites/Stars, Premium styling, Sort cycling), Model Auto-Fetch, and robust background download management. Completed Sounds panel with Alert Volume Controller and Custom Alerts. Added StarPilotState singleton, InputDialog, Lateral full implementation (5 sub-panels), 11 value factory functions, Params helpers, and sound testing architecture.
---
## Table of Contents
1. [High-Level Architecture Comparison](#1-high-level-architecture-comparison)
2. [Key Parallels: Qt to Raylib](#2-key-parallels-qt-to-raylib)
3. [Code Structure Patterns](#3-code-structure-patterns)
4. [Onroad UI Comparison](#4-onroad-ui-comparison)
5. [Sidebar Comparison](#5-sidebar-comparison)
6. [Settings Layout Comparison](#6-settings-layout-comparison)
7. [Implementation Differences](#7-implementation-differences)
8. [Files Requiring Work for Full Port](#8-files-requiring-work-for-full-port)
- [Core Infrastructure](#81-priority-1-core-infrastructure)
- [Implementation Guide](#implementation-guide-adding-new-starpilot-panels)
- [Key Learnings](#key-learnings-from-starpilot-implementation)
- [Common Pitfalls](#common-pitfalls-and-solutions)
- [Onroad Overlays](#82-priority-2-onroad-overlays)
- [Additional Features](#83-priority-3-additional-features)
9. [StarPilot Complete Implementation Roadmap](#9-starpilot-complete-implementation-roadmap)
- [Architecture Overview](#91-architecture-overview)
- [Current Status](#92-current-status)
- [Required: Value Control Factory Functions](#93-required-value-control-factory-functions)
- [Required: Three-Level Navigation System](#94-required-three-level-navigation-system)
- [Complete Panel Implementation Details](#95-complete-panel-implementation-details)
10. [Technical Reference](#10-technical-reference)
- [Core Application Class](#101-core-application-class)
- [Widget Base Class](#102-widget-base-class)
- [UI State Management](#103-ui-state-management)
- [Environment Variables](#104-environment-variables)
- [Device Detection](#105-device-detection)
- [Widget Factory Functions](#106-widget-factory-functions-reference)
- [ListItem Class](#107-listitem-class-reference)
11. [RayGUI Analysis (Aborted)](#11-raygui-analysis-aborted)
---
## 1. High-Level Architecture Comparison
### 1.1 Qt UI Structure (Current Legacy)
```
MainWindow (QStackedLayout)
├── HomeWindow (QHBoxLayout)
│ ├── Sidebar (fixed 300px width)
│ └── QStackedLayout
│ ├── OffroadHome (home.cc)
│ ├── OnroadWindow (onroad_home.cc)
│ ├── BodyWindow (body.cc)
│ └── DriverViewWindow (driverview.cc)
├── SettingsWindow (QStackedWidget)
│ └── Multiple panels (Device, Network, Toggles, Software, Firehose, Developer)
└── OnboardingWindow
```
**Entry Point:** `selfdrive/ui/main.cc` → compiled binary `selfdrive/ui/ui`
**Key Files:**
- `selfdrive/ui/qt/window.cc` (Main window management)
- `selfdrive/ui/qt/home.cc` (Home window container)
- `selfdrive/ui/qt/sidebar.cc` (Sidebar implementation)
- **StarPilot Qt Panels**: `starpilot/ui/qt/offroad/starpilot_settings.cc/h` (plus `lateral_settings.cc`, `longitudinal_settings.cc`, etc.)
- **Raylib Port**: `selfdrive/ui/layouts/settings/starpilot` (Modularized!)
- **UI State Singleton**: `selfdrive/ui/lib/starpilot_state.py` (Centralized `StarPilotState`)
- `selfdrive/ui/qt/offroad/settings.cc` (Settings panels)
### 1.2 Raylib Big UI Structure (Current)
```
MainLayout (Widget)
├── Sidebar (300px width - selfdrive/ui/layouts/sidebar.py)
└── State Machine (MainState enum)
├── HOME → HomeLayout (selfdrive/ui/layouts/home.py)
├── SETTINGS → SettingsLayout (selfdrive/ui/layouts/settings/settings.py)
└── ONROAD → AugmentedRoadView (selfdrive/ui/onroad/augmented_road_view.py)
```
The StarPilot Raylib Big UI is built on a modular "Panel" architecture.
The main entry point is `StarPilotLayout` (in `main_panel.py`), which manages a stack of `StarPilotPanel` instances.
**Entry Point:** `selfdrive/ui/ui.py` (lines 13-36)
**Key Files:**
- `selfdrive/ui/ui.py` (Entry point)
- `selfdrive/ui/layouts/main.py` (Main layout container)
- `selfdrive/ui/layouts/sidebar.py` (Sidebar implementation)
- `system/ui/lib/application.py` (GuiApplication class)
### 1.3 Device Resolution Reference
| Device | UI Framework | Resolution |
|--------|-------------|------------|
| TICI/TIZI | Qt (C++) | 2160x1080 |
| MICI/PC | Raylib (Python) | Big: 2160x1080 / Small: 536x240 |
**Note:** The Raylib UI is used on ALL devices (TICI, TIZI, MICI, PC). Big UI (2160x1080) is shown on TICI/TIZI or when BIG=1 environment variable is set.
---
## 2. Key Parallels: Qt to Raylib
### 2.1 Navigation & Layout
| Qt Component | Raylib Equivalent | File Location |
|-------------|-------------------|---------------|
| `QStackedLayout` | `MainState` enum + dictionary | `layouts/main.py:15-32` |
| `QHBoxLayout` | Manual rect calculation | `layouts/main.py:61-65` |
| `QWidget.show()/hide()` | `show_event()`/`hide_event()` | `widgets/__init__.py:180-184` |
| `setFixedWidth(300)` | `SIDEBAR_WIDTH = 300` | `layouts/sidebar.py:12` |
| `setVisible(bool)` | `set_visible(bool)` | `widgets/__init__.py:66-67` |
| `QStackedWidget` | Dictionary + `_set_current_layout()` | `layouts/main.py:83-87` |
### 2.2 Widget System
| Qt Widget | Raylib Widget | File Location |
|-----------|---------------|--------------|
| `QFrame` | `Widget` (base class) | `widgets/__init__.py:22-184` |
| `QPushButton` | `Button` | `widgets/button.py:12-24` |
| `QToggle` | `Toggle` | `widgets/toggle.py:17-79` |
| `QLabel` | `Label`, `MiciLabel`, `UnifiedLabel` | `widgets/label.py:30+` |
| Custom paint | `_render()` method | All widgets |
| `mousePressEvent` | `_handle_mouse_event()` | `widgets/__init__.py:114-153` |
| `QLayout` | Manual rect calculation | Per layout file |
### 2.3 Settings Panels
| Qt Panel | Raylib Panel | Status | Location |
|----------|--------------|--------|----------|
| `DevicePanel` | `DeviceLayout` | ✅ Implemented | `layouts/settings/device.py` |
| `NetworkPanel` | `NetworkUI` | ✅ Implemented | `system/ui/widgets/network.py` |
| `TogglesPanel` | `TogglesLayout` | ⚠️ Basic only | `layouts/settings/toggles.py` |
| `SoftwarePanel` | `SoftwareLayout` | ✅ Implemented | `layouts/settings/software.py` |
| `FirehosePanel` | `FirehoseLayout` | ✅ Implemented | `layouts/settings/firehose.py` |
| `DeveloperPanel` | `DeveloperLayout` | ✅ Implemented | `layouts/settings/developer.py` |
### 2.4 StarPilot Qt Panels
| Qt Panel | Purpose | Qt File Location | Raylib Status |
|----------|---------|-----------------|---------------|
| `StarPilotDataPanel` | Data settings | `starpilot/ui/qt/offroad/data_settings.cc` | 🟡 Implemented (Stub) |
| `StarPilotDevicePanel` | Device controls | `starpilot/ui/qt/offroad/device_settings.cc` | 🟡 Implemented (Stub) |
| `StarPilotLateralPanel` | Steering controls | `starpilot/ui/qt/offroad/lateral_settings.cc` | ✅ Implemented (5 sub-panels with real controls) |
| `StarPilotLongitudinalPanel` | Gas/Brake controls | `starpilot/ui/qt/offroad/longitudinal_settings.cc` | 🟡 Implemented (Weather sub-panels with real value controls) |
| `StarPilotMapsPanel` | Map data | `starpilot/ui/qt/offroad/maps_settings.cc` | 🟡 Implemented (Stub) |
| `StarPilotModelPanel` | Driving model | `starpilot/ui/qt/offroad/model_settings.cc` | ✅ Implemented (Auto-fetch, Selection, Download, Favorites, Sort) |
| `StarPilotNavigationPanel` | Navigation | `starpilot/ui/qt/offroad/navigation_settings.cc` | 🟡 Implemented (Stub) |
| `StarPilotSoundsPanel` | Alerts and sounds | `starpilot/ui/qt/offroad/sounds_settings.cc` | ✅ Implemented (Alert Volume Controller + Custom Alerts sub-panels) |
| `StarPilotThemesPanel` | Theme settings | `starpilot/ui/qt/offroad/theme_settings.cc` | 🟡 Implemented (MANAGE buttons) |
| `StarPilotUtilitiesPanel` | Utilities | `starpilot/ui/qt/offroad/utilities.cc` | 🟡 Implemented (Stub) |
| `StarPilotVehiclesPanel` | Vehicle settings | `starpilot/ui/qt/offroad/vehicle_settings.cc` | 🟡 Implemented (Stub) |
| `StarPilotVisualsPanel` | Appearance/Visuals | `starpilot/ui/qt/offroad/visual_settings.cc` | 🟡 Implemented (MANAGE buttons) |
| `StarPilotWheelPanel` | Wheel controls | `starpilot/ui/qt/offroad/wheel_settings.cc` | 🟡 Implemented (Stub) |
**Main Entry:** `starpilot/ui/qt/offroad/starpilot_settings.cc` - Contains category navigation
**Raylib Implementation:** `selfdrive/ui/layouts/settings/starpilot.py` - Full sub-panel hierarchy implemented
**Qt Back Navigation:**
- Uses Qt signals: `closeSubPanel()`, `closeSubSubPanel()` signals
- Parent `SettingsWindow` emits these signals, child panels listen and respond
- Each sub-panel has a way to emit signal to return to previous view
### 2.5 StarPilot Raylib Folders
- `selfdrive/ui/layouts/settings/starpilot/` (Directory containing 14+ setting modules)
### 2.6 StarPilot Key Parallels
- **Qt `StackedLayout`** ≈ **Raylib `_panels` dictionary in `main_panel.py`**
- **Qt `StarPilotPanelType`** ≈ **Raylib `StarPilotPanelType` in `panel.py`** (Integer-based routing)
- **Qt `Panel::showEvent()`** ≈ **Raylib `Widget.show_event()`** (Used for dynamic range calculation)
---
## 3. Code Structure Patterns
### 3.1 Signal/Slot vs Callback Pattern
#### Qt Signal/Slot
```cpp
// From selfdrive/ui/qt/home.cc
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings);
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings);
QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings);
```
#### Raylib Callback
```python
# From selfdrive/ui/layouts/main.py
def _setup_callbacks(self):
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
on_flag=self._on_bookmark_clicked,
open_settings=lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(...)
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state)
```
### 3.2 Widget Callback Registration
#### Qt
```cpp
// From selfdrive/ui/qt/widgets/toggle.cc
QObject::connect(toggle, &Toggle::stateChanged, this, &TogglesPanel::toggleToggled);
```
#### Raylib
```python
# From system/ui/widgets/toggle.py
class Toggle(Widget):
def __init__(self, initial_state: bool = False, callback: Callable[[bool], None] | None = None):
self._callback = callback
def _handle_mouse_release(self, mouse_pos: MousePos):
self._state = not self._state
if self._callback:
self._callback(self._state)
```
### 3.3 Paint Event vs Render Method
#### Qt Custom Paint
```cpp
// From selfdrive/ui/qt/sidebar.cc
void Sidebar::drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y) {
const QRect rect = {30, y, 240, 126};
p.setPen(Qt::NoPen);
p.setBrush(QBrush(c));
p.drawRoundedRect(QRect(rect.x() + 4, rect.y() + 4, 100, 118), 18, 18);
}
```
#### Raylib Render
```python
# From selfdrive/ui/layouts/sidebar.py
def _draw_metric(self, rect: rl.Rectangle, label: str, value: str, color: rl.Color):
# Draw colored bar
bar_rect = rl.Rectangle(rect.x + 4, rect.y + 4, 100, 118)
rl.draw_rectangle_rounded(bar_rect, 1.0, 18, color)
```
### 3.4 Toggle Implementation Comparison
#### Qt Toggle (`widgets/toggle.cc`)
- Uses `QPropertyAnimation` for knob animation
- Custom painting with `QPainter`
- 80px height, variable width
- Green (#178644) when ON, Gray (#393939) when OFF
#### Raylib Toggle (`widgets/toggle.py`)
```python
WIDTH, HEIGHT = 160, 80
BG_HEIGHT = 60
ANIMATION_SPEED = 8.0
ON_COLOR = rl.Color(51, 171, 76, 255) # Green
OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) # Dark gray
class Toggle(Widget):
def _render(self, rect: rl.Rectangle):
# Background
bg_rect = rl.Rectangle(self._rect.x + 5, self._rect.y + 10, WIDTH - 10, BG_HEIGHT)
rl.draw_rectangle_rounded(bg_rect, 1.0, 10, bg_color)
# Knob
knob_x = self._rect.x + HEIGHT / 2 + (WIDTH - HEIGHT) * self._progress
rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color)
```
### 3.5 State Management Pattern
#### Qt State
```cpp
// From selfdrive/ui/qt/home.cc
void HomeWindow::updateState(const UIState &s, const StarPilotUIState &fs) {
if (s.scene.started) {
if (starpilot_scene.driver_camera_timer >= UI_FREQ / 2) {
showDriverView(true, true);
} else {
slayout->setCurrentWidget(onroad);
}
}
}
```
#### Raylib State
```python
# From selfdrive/ui/layouts/main.py
class MainState(IntEnum):
HOME = 0
SETTINGS = 1
ONROAD = 2
class MainLayout(Widget):
def _handle_onroad_transition(self):
if ui_state.started != self._prev_onroad:
self._prev_onroad = ui_state.started
self._set_mode_for_state()
def _set_mode_for_state(self):
if ui_state.started:
self._set_current_layout(MainState.ONROAD)
else:
self._set_current_layout(MainState.HOME)
```
---
## 4. Onroad UI Comparison
### 4.1 Qt Onroad Components
```
OnroadWindow (onroad_home.cc)
├── AnnotatedCameraWidget (camera + lane lines + path)
│ ├── ModelRenderer (lane lines, path predictions)
│ └── HudRenderer (speed, cruise control)
├── OnroadAlerts (alert messages)
├── StarPilotAnnotatedCameraWidget (StarPilot overlays)
└── StarPilotOnroadWindow (additional overlays)
├── Blind spot visualization
├── FPS counter
├── Steering torque metrics
└── Turn signal indicators
```
**Key Files:**
- `selfdrive/ui/qt/onroad/onroad_home.cc`
- `selfdrive/ui/qt/onroad/annotated_camera.cc`
- `selfdrive/ui/qt/onroad/hud.cc`
- `selfdrive/ui/qt/onroad/alerts.cc`
- `selfdrive/ui/qt/onroad/buttons.cc`
- `starpilot/ui/qt/onroad/starpilot_onroad.cc`
### 4.2 Raylib Onroad Components
```
AugmentedRoadView (augmented_road_view.py - extends CameraView)
├── ModelRenderer (lane lines, path predictions) ✅
├── HudRenderer (speed, cruise control, status) ✅
├── AlertRenderer (driving alerts) ✅
└── DriverStateRenderer (driver monitoring) ✅
```
**Key Files:**
- `selfdrive/ui/onroad/augmented_road_view.py` (234 lines)
- `selfdrive/ui/onroad/model_renderer.py`
- `selfdrive/ui/onroad/hud_renderer.py` (180 lines)
- `selfdrive/ui/onroad/alert_renderer.py`
- `selfdrive/ui/onroad/driver_state.py`
- `selfdrive/ui/onroad/cameraview.py`
### 4.3 Onroad Features: Qt vs Raylib
| Feature | Qt | Raylib | Notes |
|---------|-----|--------|-------|
| Camera feed | ✅ | ✅ | |
| Lane lines | ✅ | ✅ | |
| Path predictions | ✅ | ✅ | |
| Speed display | ✅ | ✅ | |
| Cruise control | ✅ | ✅ | |
| Alert messages | ✅ | ✅ | |
| Driver monitoring | ✅ | ✅ | |
| Blind spot metrics | ✅ | ❌ | Needs port |
| FPS counter | ✅ | ❌ | Needs port |
| Steering torque | ✅ | ❌ | Needs port |
| Turn signals | ✅ | ❌ | Needs port |
| Theme support | ✅ | ⚠️ | Partial |
---
## 5. Sidebar Comparison
### 5.1 Qt Sidebar (`qt/sidebar.cc` - 302 lines)
```cpp
// Fixed 300px width
Sidebar::Sidebar(QWidget *parent) : QFrame(parent) {
setFixedWidth(300);
// Metrics displayed:
// - Temperature (with color coding)
// - CPU usage (with tap-to-cycle)
// - Memory usage (with tap-to-cycle)
// - Panda status
// - Network status
// Buttons:
// - Home button (180x180)
// - Flag button
// - Settings button (200x117)
}
```
### 5.2 Raylib Sidebar (`layouts/sidebar.py` - 229 lines)
```python
SIDEBAR_WIDTH = 300
METRIC_HEIGHT = 126
METRIC_WIDTH = 240
METRIC_MARGIN = 30
FONT_SIZE = 35
class Sidebar(Widget):
def __init__(self):
# Same metrics: Temperature, CPU, Memory, Panda, Network
# Same buttons with texture-based rendering
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
```
### 5.3 Differences
| Aspect | Qt | Raylib |
|--------|-----|--------|
| Width | 300px | 300px |
| Temperature | ✅ with color | ✅ with color |
| CPU | ✅ with tap cycle | ✅ basic |
| Memory | ✅ with tap cycle | ✅ basic |
| Panda | ✅ | ✅ |
| Network | ✅ | ✅ |
| Developer toggle | ✅ | ❌ |
---
## 6. Settings Layout Comparison
### 6.1 Qt Settings Structure
```cpp
// From selfdrive/ui/qt/offroad/settings.cc
SettingsWindow::SettingsWindow(QWidget *parent) {
// Left sidebar navigation (icon + text)
// Panel selector: Device, Network, Toggles, Software, Firehose, Developer
// Panel content area on right
// Scrollable content within each panel
}
```
### 6.2 Raylib Settings Structure
```python
# From selfdrive/ui/layouts/settings/settings.py
SIDEBAR_WIDTH = 500 # Wider than main sidebar
NAV_BTN_HEIGHT = 110
PANEL_MARGIN = 50
class PanelType(IntEnum):
DEVICE = 0
NETWORK = 1
TOGGLES = 2
SOFTWARE = 3
FIREHOSE = 4
DEVELOPER = 5
class SettingsLayout(Widget):
def _render(self, rect: rl.Rectangle):
# Left sidebar (500px) with nav buttons
# Right panel area
self._draw_sidebar(sidebar_rect)
self._draw_current_panel(panel_rect)
```
### 6.2.1 Settings Panel Width Comparison
| Panel | Qt Sidebar | Raylib Sidebar |
|-------|------------|----------------|
| Main Settings | 300px | 500px |
---
## 7. Implementation Differences
### 7.1 Language & Framework
| Aspect | Qt | Raylib |
|--------|-----|--------|
| Language | C++ | Python |
| Graphics API | Qt QPainter | pyray (OpenGL wrapper) |
| Event System | Qt event loop | raylib input + custom mouse thread |
### 7.2 Event Loop
#### Qt Event Loop
- Qt's built-in signal/slot mechanism
- `QApplication::exec()` runs the loop
- Events dispatched via `QObject::event()`
#### Raylib Event Loop
```python
# From selfdrive/ui/ui.py
def main():
gui_app.init_window("UI")
if gui_app.big_ui():
main_layout = MainLayout()
else:
main_layout = MiciMainLayout()
for should_render in gui_app.render(): # Generator
ui_state.update()
if should_render:
main_layout.render()
```
### 7.3 Text Rendering
#### Qt
```cpp
p.setFont(InterFont(35, QFont::DemiBold));
p.drawText(rect, Qt::AlignCenter, text);
```
#### Raylib
```python
# From system/ui/widgets/label.py
font = gui_app.font(FontWeight.NORMAL)
rl.draw_text_ex(font, text, position, font_size, spacing, color)
```
### 7.4 Layout Calculations
#### Qt (Automatic)
```cpp
QHBoxLayout *main_layout = new QHBoxLayout(this);
main_layout->setMargin(0);
main_layout->setSpacing(0);
main_layout->addWidget(sidebar);
```
#### Raylib (Manual)
```python
# From selfdrive/ui/layouts/main.py
def _update_layout_rects(self):
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
```
### 7.5 Animations
#### Qt (Property Animation)
```cpp
// From selfdrive/ui/qt/widgets/toggle.cc
_anim = new QPropertyAnimation(this, "offset_circle", this);
_anim->setStartValue(on ? left + immediateOffset : right - immediateOffset);
_anim->setEndValue(on ? right : left);
_anim->setDuration(animation_duration);
_anim->start();
```
#### Raylib (Manual Interpolation)
```python
# From system/ui/widgets/toggle.py
def update(self):
if abs(self._progress - self._target) > 0.01:
delta = rl.get_frame_time() * ANIMATION_SPEED
self._progress += delta if self._progress < self._target else -delta
self._progress = max(0.0, min(1.0, self._progress))
```
---
## 8. Files Requiring Work for Full Port
### 8.1 Priority 1: Core Infrastructure
| File | Description | Status |
|------|-------------|--------|
| `selfdrive/ui/layouts/settings/` | StarPilot panels | 🟡 Structure Done (stubs for most, Lateral fully implemented) |
| `system/ui/widgets/toggle.py` | Add description support | ✅ Done |
| `system/ui/widgets/input_dialog.py` | Text input dialog with keyboard | ✅ Done (NEW) |
| `system/ui/widgets/selection_dialog.py` | Radio-button selection dialog | ✅ Done (NEW) |
| `system/ui/widgets/confirm_dialog.py` | Confirm/alert dialog with rich text | ✅ Done (UPDATED) |
| `selfdrive/ui/lib/starpilot_state.py` | Car state + StarPilotState singleton | ✅ Done (NEW) |
| `common/params.py` | Params get_int/get_float/put_int/put_float | ✅ Done (UPDATED) |
| `selfdrive/ui/layouts/sidebar.py` | Developer metrics toggle | 🔴 Not Started |
### 8.1.1 StarPilot Panel Hierarchy (Implemented)
```
StarPilot Settings
├── Tuning Level ✅
├── Category: Alerts and Sounds → SOUNDS ✅ (Alert Volume Controller + Custom Alerts sub-panels with real controls)
│ ├── Alert Volume Controller ✅ (7 volume sliders with Test buttons, persistent subprocess for offroad sound testing)
│ └── Custom Alerts ✅ (5 toggles with conditional visibility: BSM, ShowSpeedLimits/SpeedLimitController)
├── Category: Driving Controls
│ ├── DRIVING_MODEL ✅ (Auto-fetch, Premium SelectionDialog, Managed background downloads, Blacklist/Ratings)
│ ├── LONGITUDINAL 🟡 (MANAGE buttons + Weather sub-panels with real value_item controls)
│ └── LATERAL ✅ (5 sub-panels with REAL controls: value_item, value_button_item, toggle_item)
│ ├── Advanced Lateral Tuning ✅ (8 controls: 5 value_button_item + 3 toggle_item)
│ ├── Always On Lateral ✅ (3 controls)
│ ├── Lane Changes ✅ (6 controls with conditional visibility)
│ ├── Lateral Tuning ✅ (3 toggles with reboot confirmation)
│ └── Quality of Life ✅ (1 value_button_item with sub-toggle)
├── Category: Navigation
│ ├── MAPS 🟡 (stub)
│ └── NAVIGATION 🟡 (stub)
├── Category: System Settings
│ ├── DATA 🟡 (stub)
│ ├── DEVICE 🟡 (stub)
│ └── UTILITIES 🟡 (stub)
├── Category: Theme and Appearance
│ ├── VISUALS 🟡 (5 MANAGE buttons)
│ └── THEMES 🟡 (stub)
└── Category: Vehicle Settings
├── VEHICLE 🟡 (stub)
└── WHEEL 🟡 (stub)
```
Legend: ✅ = Full | 🟡 = Structure/stub | 🔴 = Not started
#### Completed (March 16, 2026)
- Added `StarPilotLayout` entry to `settings/settings.py`
- Created `layouts/settings/starpilot.py` with full sub-panel navigation system
- Added `StarPilotPanelType` enum with 14 panel types for proper sub-panel routing:
- MAIN, SOUNDS, DRIVING_MODEL, LONGITUDINAL, LATERAL, MAPS, NAVIGATION,
- DATA, DEVICE, UTILITIES, VISUALS, THEMES, VEHICLE, WHEEL
- Implemented `StarPilotSoundsLayout` with 6 toggle items for custom alerts
- Implemented `StarPilotDrivingModelLayout` with toggles + buttons
- Implemented `StarPilotLongitudinalLayout`, `StarPilotLateralLayout` with MANAGE buttons
- Implemented stub layouts for all remaining panels (Maps, Navigation, Data, Device, Utilities, Visuals, Themes, Vehicle, Wheel)
- Added tuning level visibility filtering to sub-panels (`set_tuning_levels()`, `refresh_visibility()`)
- Uses `button_item` with "MANAGE" button text matching Qt UI
- Added `starpilot_texture()` method to `GuiApplication` for loading StarPilot assets
- Added `starpilot_icon` parameter to `button_item()` and `toggle_item()` for StarPilot icon support
- StarPilot assets remain in `starpilot/assets/` (not copied)
- Descriptions use HTML bold tags (`<b>...</b>`) matching Qt format
- Toggle callbacks properly persist settings via Params
- **Hierarchical back navigation**: Settings sidebar back button handles depth > 0 (go back) vs depth 0 (close)
- **Category buttons**: Fixed with per-button width calculation + cumulative positioning + matching width_hint
#### Completed (March 17, 2026)
- **Created `StarPilotState` singleton** (`selfdrive/ui/lib/starpilot_state.py`) with:
- `StarPilotCarState` dataclass with all car type, capability, and value fields
- Reads `CarParamsPersistent`, `StarPilotCarParamsPersistent`, `LiveTorqueParameters`, `StarPilotToggles` from Params
- Throttled updates (2.0s interval) to avoid slowing UI
- PC/desktop fallback mode with configurable car make/model
- Global import: `from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state`
- **Fully implemented Lateral panel** with 5 sub-panels and real controls:
- `StarPilotAdvancedLateralLayout`: 5 `value_button_item` controls with car-specific dynamic ranges (`starpilot_state.car_state.steerKp * 0.5` to `* 1.5`) + Reset buttons + 3 toggle items with conditional visibility based on car state
- `StarPilotAlwaysOnLateralLayout`: 2 toggles (with reboot confirmation) + 1 value slider
- `StarPilotLaneChangesLayout`: 6 controls with conditional visibility (`LaneChanges AND NudgelessLaneChange`)
- `StarPilotLateralTuneLayout`: 3 toggles (NNFF, NNFFLite, TurnDesires) with reboot confirmation
- `StarPilotLateralQOLLayout`: 1 `value_button_item` with sub-toggle
- **Weather sub-panels now use real `value_item()` controls** with `put_int()` callbacks
- **Created `InputDialog` widget** (`system/ui/widgets/input_dialog.py`):
- Full keyboard input with `Keyboard` widget integration
- Text input field with hint text, blinking cursor
- Confirm/Cancel buttons, dimmed background overlay
- Callback: `on_close(DialogResult, str)` returns entered text
- **Created `SelectionDialog` widget** (`system/ui/widgets/selection_dialog.py`):
- Radio-button selection from a list of options
- Scrollable list using `Scroller` widget
- Visual feedback: green dot for selected, circle outline for unselected
- Callback: `on_close(DialogResult, int, str)` returns index and text
- **Extended `Params` wrapper** (`common/params.py`):
- `get_int(key, block, return_default, default)` — Parses string param as int
- `get_float(key, block, return_default, default)` — Parses string param as float
- `put_int(key, val)` — Type-aware save (checks `get_type()` for FLOAT/INT/BOOL/string)
- `put_float(key, val)` — Same type-aware save for float values
- Fixes `TypeError` when saving FLOAT-typed params like `IncreaseFollowingLowVisibility`
- **Updated `ConfirmDialog`** (`system/ui/widgets/confirm_dialog.py`):
- Added `rich` mode with `HtmlRenderer` + `Scroller` for rich text content
- Scrollable text area for long messages
- Keyboard shortcut support: Enter (confirm), Escape (cancel)
- **Settings back button icon changed** from `icons/close2.png``icons/backspace.png`
- **Zero-size image guard** in `application.py`: `_load_image_from_path` now returns early if `image.width == 0 or image.height == 0`, preventing potential crashes from empty textures
#### Completed (March 17, 2026 - Late Session)
- **Fully Implemented Driving Model Panel**:
- Integrated `ModelManager` for auto-fetching model lists if empty.
- Implemented managed background download thread in `starpilot.py` for desktop/PC environments (Daemon-less).
- Added blacklist and ratings management dialogs.
- **High-Fidelity SelectionDialog Enhancements**:
- **Favorites (Star) System**: Every item can be starred (♥/♡). Persisted in `UserFavorites` and `CommunityFavorites`.
- **Premium Styling**: Matched Qt header (`#333333` BG) and selection (`#465BEA` BG + 3px white border).
- **Cyclic Sort Mode**: Sort button cycles through: Alphabetical -> Newest -> Oldest -> Favorites First.
- **Disclosure Triangles**: Used text-based symbols (`▶`/`▼`) for collapsible categories to match Qt without complex mesh drawing.
- **Width Inheritance**: Fixed hit-testing and alignment by ensuring list items span the full width of the scroller.
#### Completed (March 17, 2026) - Sounds Panel Full Implementation
- **Sounds panel restructured** to use sub-panel navigation:
- Main panel has 2 MANAGE buttons: "Alert Volume Controller" and "StarPilot Alerts"
- `StarPilotVolumeControlLayout`: 7 volume sliders (Disengage, Engage, Prompt, PromptDistracted, Refuse, WarningSoft, WarningImmediate)
- `StarPilotCustomAlertsLayout`: 5 toggles (GoatScream, GreenLightAlert, LeadDepartingAlert, LoudBlindspotAlert, SpeedLimitChangedAlert)
- **Volume slider features**:
- Range 0-101 (0=Muted, 101=Auto, 1-100=percentage)
- WarningSoft/WarningImmediate have min=25 (can't go below 25%)
- Each slider has "Test" button to preview sound
- **Sound testing architecture** (critical fix):
- **Offroad**: Uses persistent Python subprocess (matching Qt's `initializeSoundPlayer()`)
- Subprocess runs a while loop reading `path|volume\n` lines from stdin
- Uses `sounddevice` to play WAV files with volume scaling
- No threading - stdin automatically serializes writes (subprocess isolation prevents memory corruption)
- Theme sounds take priority over stock sounds (`ACTIVE_THEME_PATH / "sounds"`)
- **Onroad**: Uses `TestAlert` param in `params_memory` (handled by `soundd.py`)
- **Custom Alerts conditional visibility**:
- `LoudBlindspotAlert`: Visible only if `starpilot_state.car_state.hasBSM`
- `SpeedLimitChangedAlert`: Visible if `ShowSpeedLimits` OR (`hasOpenpilotLongitudinal` AND `SpeedLimitController`)
- **Sub-panel navigation**: `_setup_sounds_sub_panels()` in `StarPilotLayout` wires up navigation callbacks
### 8.1.1 Implementation Guide - Adding New StarPilot Sub-Panels (The Modular Way)
Since the StarPilot settings split (March 2026), adding or updating panels follows a modular file-based pattern:
1. **Create a new module** in `selfdrive/ui/layouts/settings/starpilot/` (e.g., `new_feature.py`).
2. **Inherit from `StarPilotPanel`** (from `panel.py`):
```python
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
class StarPilotNewFeatureLayout(StarPilotPanel):
def __init__(self):
super().__init__()
# Build Layout...
```
3. **Restoring Base Class Methods (IMPORTANT)**: If you override `__init__`, always check if you need to restore core methods that the monolithic refactor moved to the base class:
- `self.set_tuning_levels()`
- `self._update_state()` (for Driving Model)
- `self.refresh_visibility()`
4. **Register in `main_panel.py`**:
- Import the new class.
- Add a key to `StarPilotPanelType`.
- Add to the `_panels` dictionary in `StarPilotLayout`.
5. **Update `refresh_visibility` in `main_panel.py`**: Ensure the new panel is included in the loop to react to tuning level changes.
#### Key Learning: Large File Prevention
Don't let any single panel layout exceed ~500 lines. If a panel has many sub-sub-panels (like Longitudinal), break those into their own classes or separate files (e.g. `longitudinal_tuning.py`).
#### Key Learnings from StarPilot Implementation
1. **Native Pattern is Critical**: Always use `Scroller` + factory functions (`button_item`, `toggle_item`, etc.) - never draw custom rectangles. This ensures:
- Consistent styling with other panels
- Proper scrolling behavior
- Built-in description toggle on tap
- Proper touch/mouse event handling
2. **Sub-Panel Navigation & Boilerplate Reduction**: StarPilot uses a DRY architecture via the `StarPilotPanel(Widget)` base class:
- Inheriting from `StarPilotPanel` automatically provides: `self._params`, `self._params_memory`, `self._tuning_levels`, and navigation lifecycle functions.
- It also automatically handles standard `_render()` and `show_event()` routing for `self._scroller` and `self._sub_panels` maps.
- Main categories route to sub-panels via a two-level navigation system using `StarPilotPanelType` enums.
3. **Asset Loading for External Packages**:
- Use `gui_app.texture()` for openpilot assets (`selfdrive/assets/`)
- Use `gui_app.starpilot_texture()` for assets located in the core `starpilot` assets folder.
```python
item.set_icon("icon_steering.png", starpilot=True) # Uses starpilot_texture internally
```
- Never copy assets between folders - always load from original location
- Note: StarPilot toggle icons are in `starpilot/assets/toggle_icons/`
4. **Icon Handling in List Items**:
```python
# For StarPilot icons, use starpilot_icon=True parameter
item = button_item(
title_fn, button_fn, desc_fn,
icon="toggle_icons/icon_sound.png", # Full path from assets root
starpilot_icon=True, # This flag is critical!
)
```
5. **Description Formatting**: Use HTML bold tags to match Qt:
```python
description=tr_noop("<b>Description text here</b>")
```
6. **Button Text Patterns**:
- Navigation/management: "MANAGE", "VIEW", "CHANGE"
- NOT ">" or similar symbols
7. **Python 3.12 Compatibility**:
- Use `from __future__ import annotations` at top of files
- Use `Optional[X]` instead of `X | None` for type hints
- Use `from typing import Callable` for callbacks
8. **Known Issues**:
- Empty string handling in `set_icon()` can cause division by zero if texture loading fails
- Icons from StarPilot assets require `starpilot_icon=True` parameter
9. **Back Navigation Pattern**:
- In Raylib, there's no built-in "back" button like in Qt's stacked layout
- Each sub-panel needs a "Back" button to return to the parent/main view
- Pattern: Create a callback in the parent layout (`_back_to_main`) and pass it to child layouts
- Use `button_item` with a "<" or "Back" button text for the back action
- See `starpilot.py` for the complete implementation
10. **Premium UI "TLC" (High-Fidelity Parity)**:
- **Colors**: Use specific Qt hex codes (e.g., `#333333` for headers, `#465BEA` for selection).
- **Hit Testing**: Ensure buttons/items span the full width of their container. Users expect to be able to click anywhere on the row, not just the text.
- **Transitions**: When a download starts, the UI should immediately reflect it (e.g., "CANCEL" button instead of "DOWNLOAD").
- **Symbols**: Raylib handles Unicode characters well—use fonts that support characters like ``, ``, ``, and `` for quick, clean UI elements.
11. **Background Process Management**:
- **Daemon-less Downloads**: On PC/Desktop where `starpilot_process.py` isn't running, the UI must manage its own background threads.
- **Thread Safety**: Always wrap long-running UI-initiated threads in `try-except` blocks. If they crash, they fail silently in Raylib.
- **Progress Tracking**: Poll `Params` (memory) for progress updates (`ModelDownloadProgress`) to keep the UI responsive.
12. **Tuning Level System & Developer Panel Visibility**
The Qt UI has a sophisticated tuning level system that controls which StarPilot settings are visible:
- **Tuning Levels**: 0=Minimal, 1=Standard, 2=Advanced, 3=Developer
- Each StarPilot toggle has a minimum required tuning level (stored in `starpilotToggleLevels` map)
- Toggle visibility: `tuningLevel >= starpilotToggleLevels[key]`
**IMPORTANT - Two Different "Developer" Concepts:**
- **"Developer" Tuning Level** (level 3 in StarPilot's Tuning Level selector): The highest level that unlocks ALL StarPilot settings
- **"Developer" Panel** (separate panel in Settings sidebar): ONLY visible when Tuning Level >= 3
These are different features that share the same name! This matches Qt behavior:
```cpp
// From settings.cc - Developer panel visibility
void SettingsWindow::updateDeveloperToggle(int tuningLevel) {
for (QAbstractButton *btn : nav_btns->buttons()) {
if (btn->text() == tr("Developer")) {
btn->setVisible(tuningLevel >= 3);
break;
}
}
}
```
**Implementation in Raylib:**
- `StarPilotLayout` stores reference to `SettingsLayout` via `set_settings_layout()`
- When tuning level changes, calls `settings_layout.refresh_developer_visibility()`
- Developer panel checked dynamically on each render
13. **Category Buttons with Horizontal Layout**
Qt displays multiple buttons horizontally next to each category title. Implemented in Raylib using a new custom action:
**New Component: `CategoryButtonsAction`**
```python
# In system/ui/widgets/list_view.py
class CategoryButtonsAction(ItemAction):
def __init__(self, buttons, button_width=180, enabled=True):
# Auto-sizes buttons based on text content
# Renders multiple buttons horizontally
```
**Factory Function:**
```python
category_buttons_item(
title="Category Title",
buttons=[("BUTTON1", callback1), ("BUTTON2", callback2)],
description="Description text",
icon="icon.png",
starpilot_icon=True,
)
```
**CATEGORIES Structure** (in `starpilot.py`):
```python
CATEGORIES = [
{"title": "Alerts and Sounds", "icon": "icon_sound.png", "desc": "...",
"buttons": [("MANAGE", "SOUNDS", 0)]},
{"title": "Driving Controls", "icon": "icon_steering.png", "desc": "...",
"buttons": [("DRIVING MODEL", "DRIVING_MODEL", 0), ("GAS / BRAKE", "LONGITUDINAL", 0), ("STEERING", "LATERAL", 0)]},
# ... etc
]
```
**IMPORTANT - Each Button Needs Unique Panel Type**:
- Qt uses stacked layouts where each category button switches to a different panel
- Originally we made the mistake of mapping all 3 Driving buttons to the same "DRIVING" panel
- Fix: Each button must map to its own unique `StarPilotPanelType` (DRIVING_MODEL, LONGITUDINAL, LATERAL)
- The panel dictionary must have entries for each unique panel type
Each button tuple is: `(button_label, panel_key, min_level)`
- `button_label`: Text shown on button
- `panel_key`: Which panel to open
- `min_level`: Minimum tuning level required to see this button
**Tuning Level Filtering:**
- Level 0 (Minimal): Shows buttons with min_level 0
- Level 1 (Standard): Shows buttons with min_level 0 or 1
- Level 2 (Advanced): Shows buttons with min_level 0, 1, or 2
- Level 3 (Developer): Shows all buttons
#### StarPilot Panel Categories (6 Main Categories)
| Category | Qt Panel | Raylib File | Sub-panels |
|----------|----------|-------------|------------|
| Alerts and Sounds | `StarPilotSoundsPanel` | `sounds.py` | 1 |
| Driving Controls | `DrivingModel`, `Lateral`, `Longitudinal` | `driving_model.py`, `lateral.py`, `longitudinal.py` | 5 |
| Navigation | `Maps`, `Navigation` | `maps.py`, `navigation.py` | 1 |
| System Settings | `Data`, `Device`, `Utilities` | `data.py`, `device.py`, `utilities.py` | 3 |
| Theme and Appearance | `Visuals`, `Themes` | `visuals.py`, `themes.py` | 2 |
| Vehicle Settings | `Vehicles`, `Wheel` | `vehicle.py`, `wheel.py` | 2 |
**Total: 14 modules across 6 categories**
#### Common Pitfalls and Solutions
| Issue | Cause | Solution |
|-------|-------|----------|
| Division by zero when loading texture | Empty string passed to texture loader, or file doesn't exist | Pass empty string to ListItem init, then call `set_icon(path, starpilot=True)` separately. Ensure StarPilot icons use `starpilot_icon=True` flag |
| Icons not loading | Wrong asset path or missing starpilot flag | Use full path (`toggle_icons/icon.png`) and `starpilot_icon=True` |
| UI doesn't match native panels | Custom drawing instead of Scroller | Always use Scroller + factory functions |
| Button text wrong | Using ">" instead of "MANAGE" | Use descriptive button text from Qt UI |
| Descriptions don't render | Missing HTML tags | Use `<b>...</b>` tags in descriptions |
| Sub-panel navigation | Need to switch between sub-panels | Use `StarPilotPanelType` enum + dictionary pattern like in `starpilot.py` |
| **No way to go back** | Sub-panels have no back button | Use hierarchical back button in settings sidebar - implements global back navigation (depth > 0 = go back, depth 0 = close settings) |
| **Confusing "Developer" naming** | Two different features with same name | Remember: "Developer" Tuning Level (level 3) controls StarPilot toggle visibility; "Developer" Panel (Settings sidebar) only appears at Tuning Level >= 3 |
| **Toggle visibility not filtering** | Sub-panels don't check tuning level | Implement `set_tuning_levels()` and `refresh_visibility()` methods in each sub-panel layout |
| **Category buttons not side-by-side** | Need horizontal button layout next to title | Use `category_buttons_item()` factory function with `CategoryButtonsAction` |
| **Each category button opens same panel** | Wrong - all 3 Driving buttons opened same panel | Each button needs unique panel_key in CATEGORIES + unique StarPilotPanelType enum value + panel dict entry |
| **Category buttons overlapping title** | CategoryButtonsAction positioning was wrong | Buttons should start at `rect.x` (left edge of action rect), NOT `rect.x + rect.width`. The action rect is already positioned at the right side of the item by `get_right_item_rect`. |
| **Category buttons text overflow** | Text like "DRIVING MODEL" (~220px) overflows fixed-width 150px buttons | **Solution**: Per-button width based on text + padding (20px), scaled proportionally to fit available space. Algorithm: (1) For each button, calculate `ideal_width = text_width + 20px`, (2) Calculate total ideal width + spacing, (3) If total > available space, scale all widths proportionally, else use ideal widths. BUTTON_FONT_SIZE = 35 (matches Qt). |
| **Category buttons overlapping/gaps** | Button positioning used wrong formula: `i * (btn_w + spacing)` instead of cumulative sum. This caused buttons to overlap or have inconsistent gaps. | **Fix**: Use cumulative positioning. Initialize `current_button_x = rect.x`, then after drawing each button, advance: `current_button_x += btn_w + spacing`. This correctly positions each button after the previous one. Example with 3 buttons (widths: 146, 128, 92, spacing: 20): Button1@rect.x, Button2@rect.x+166, Button3@rect.x+314. Total width: 406px with proper 20px gaps. |
| **Category buttons overflow (Vehicle Settings)** | width_hint in __init__ used fixed 150px per button, but _render used dynamic text-based widths. Mismatch caused incorrect space allocation, leading to overflow/overlap. | **Fix**: Calculate width_hint in __init__ the same way as _render: iterate through buttons, measure text, add padding (20px), sum with spacing. Example for Vehicle Settings: "VEHICLE SETTINGS"(140px) + "WHEEL CONTROLS"(130px) + padding(40px) + spacing(20px) = ~330px. This ensures the action rect has exactly the space needed. |
| **Navigation callbacks missing** | Refactor broke manual wiring of sub-panel back buttons | **Fix**: Re-wire `panel.set_back_callback(self._go_back)` in parent layouts (`sounds.py`, `lateral.py`) |
| **`StarPilotPanel` method loss** | Refactor moved methods to base but some subclasses didn't call them | **Fix**: Ensure subclasses call `self.set_tuning_levels()` and `self._update_state()` in `__init__` before return |
| **Model lists empty** | `DrivingModel` missing `_update_model_metadata()` in `__init__` | **Fix**: Explicitly call metadata update and `_update_state` to populate lists before first render |
| **Reboot confirmation pattern** | Many toggles (NNFF, ForceTorqueController, AlwaysOnLateral) require reboot when changed while driving. | **Pattern**: Helper method `_on_reboot_toggle(key, state)` that: (1) saves param, (2) checks `ui_state.started`, (3) shows `ConfirmDialog` via `gui_app.set_modal_overlay()`, (4) calls `HARDWARE.reboot()` on confirm. |
| **Lambda capture in loops** | Creating panel callbacks in a loop (`for cat in CATEGORIES`) where `panel_type` changes but all lambdas capture the same variable. | **Fix**: Use default argument capture: `lambda p=panel_type: self._set_current_panel(p)`. Without `p=panel_type`, all callbacks would reference the last value of `panel_type`. |
| **Sound testing only first button works** | Spawning a new thread per Test button click. `sounddevice` doesn't handle concurrent `sd.play()` calls well - subsequent calls get queued/dropped, and multiple threads cause memory corruption (double free). | **Fix**: Use persistent Python subprocess (matching Qt's `initializeSoundPlayer()`). Subprocess runs a while loop reading `path|volume\n` from stdin. Each click writes to subprocess stdin - no threading locks needed since stdin serializes automatically. This is critical because sounddevice is NOT thread-safe even with locks - must run in isolated subprocess. |
### 8.2 Priority 2: Onroad Overlays
| Feature | Qt File | Status |
|---------|---------|--------|
| Blind spot visualization | `starpilot/ui/qt/onroad/starpilot_onroad.cc` | 🔴 Not Started |
| FPS counter overlay | `selfdrive/ui/onroad/hud_renderer.py` | 🟡 Partial |
| Steering torque | `selfdrive/ui/onroad/hud_renderer.py` | 🔴 Not Started |
| Turn signals | `selfdrive/ui/onroad/hud_renderer.py` | 🔴 Not Started |
### 8.3 Priority 3: Additional Features
| Feature | Qt File | Status |
|---------|---------|--------|
| Drive stats widget | `starpilot/ui/qt/widgets/drive_stats.cc` | 🔴 Not Started |
| Drive summary widget | `starpilot/ui/qt/widgets/drive_summary.cc` | 🔴 Not Started |
| Developer sidebar | `starpilot/ui/qt/widgets/developer_sidebar.cc` | 🔴 Not Started |
| Theme system | Multiple files | 🟡 Partial |
---
## 9. StarPilot Complete Implementation Roadmap
> **Note**: This roadmap was created through detailed analysis of ALL Qt source files in `starpilot/ui/qt/offroad/`. The Qt implementation has **200+ controls** across multiple levels of sub-panels with complex conditional visibility rules.
### 9.1 Architecture Overview
The StarPilot UI has a **three-level hierarchy**:
```
Level 1: Main StarPilot Panel (CATEGORY buttons)
└── Level 2: Sub-Panel (MANAGE buttons → controls)
└── Level 3: Sub-Sub-Panel (additional controls)
└── Level 4: Sub-Sub-Sub-Panel (Weather → conditions)
```
**Examples**:
- Driving Controls (category) → GAS/BRAKE (LONGITUDINAL panel) → MANAGE button → Conditional Experimental Mode sub-panel
- Driving Controls → GAS/BRAKE → MANAGE button → Weather → Low Visibility/Rain/Rainstorm/Snow (4 conditions)
**Total Controls**: 200+ toggles, buttons, and value sliders across all panels
### 9.2 Current Status
| Component | Status | Notes |
|-----------|--------|-------|
| Top-level navigation | ✅ Done | 6 categories with horizontal buttons |
| **Panel Modularization** | ✅ Done | split into 14 distinct files (March 2026) |
| StarPilotPanelType (14 types) | ✅ Done | Unique panel types for routing in `panel.py` |
| Sounds panel (toggles) | ✅ Done | Extracted to `sounds.py` |
| **Value control factory functions** | ✅ Done | 8 types: `value_item`, `value_button_item`, `dual_value_item`, `button_toggle_item`, `buttons_item`, `selection_button_item`, `label_item`, `category_buttons_item` |
| **Three-level navigation** | ✅ Done | Panel stack, navigation callbacks, Weather 4-condition sub-panels |
| **StarPilotState singleton** | ✅ Done | Real car param parsing, desktop fallback, 2s throttle |
| **InputDialog widget** | ✅ Done | Keyboard input, hint text, blinking cursor |
| **SelectionDialog widget** | ✅ Done | Radio-button selection, scrollable, green dot indicator |
| **Params helpers** | ✅ Done | get_int, get_float, put_int, put_float |
| **ConfirmDialog rich text** | ✅ Done | HtmlRenderer + Scroller, keyboard shortcuts |
| **Lateral panel** | ✅ Done | 5 sub-panels, ~21 real controls using starpilot_state |
| **Longitudinal panel** | 🟡 Partial | Weather sub-panels (4 conditions with real value_item controls) done, rest not started |
| Sounds panel (volume sliders) | ✅ Done | 7 volume controls + Test buttons + persistent subprocess for offroad sound testing |
| Driving Model panel | 🔴 Not Started | 9 toggles + 7 buttons + dialogs |
| Visual panel | 🔴 Not Started | 5 sub-panels, ~35 controls |
| Themes panel | 🔴 Not Started | Download/select for 7 theme types |
| Navigation panel | 🔴 Not Started | Mapbox keys, setup instructions |
| Data panel | 🔴 Not Started | Backups, storage, stats |
| Device panel | 🔴 Not Started | 2 sub-panels, ~14 controls |
| Vehicle panel | 🔴 Not Started | 5 sub-panels, ~20 controls |
| Wheel panel | 🔴 Not Started | 4 button controls |
| Utilities panel | 🔴 Not Started | 6+ buttons |
| Maps panel | 🔴 Not Started | Download, countries, states |
| Conditional visibility | ✅ Done | Sounds panel Custom Alerts has real conditional visibility rules (BSM, ShowSpeedLimits, SpeedLimitController). Lateral panel has car-state conditional visibility. |
| Metric unit conversion | 🟡 Partial | `is_metric=True` param on value_item exists but conversion logic needs wiring |
| Onroad overlays | 🔴 Not Started | 4 features |
| Developer sidebar | 🔴 Not Started | 1 feature |
### 9.3 Required: Value Control Factory Functions
The Qt UI uses **6 different control types** for value inputs. All need Raylib equivalents:
#### Control Types in Qt:
1. **`StarPilotParamValueControl`** - Simple value slider
- Horizontal slider with min/max/step
- Value display with unit label
- Optional custom labels (e.g., "Off", "Instant", "X seconds")
- Optional metric unit conversion
2. **`StarPilotParamValueButtonControl`** - Value slider + button
- Same as above + "Reset" or "Test" button
- Used for: SteerDelay, SteerFriction, volumes, ClusterOffset, PauseLateralSpeed, CESignalSpeed
- Button can trigger: Reset to default, Test sound, etc.
3. **`StarPilotDualParamValueControl`** - Two connected sliders
- Two value controls displayed together with shared unit
- Used for: CESpeed (Without Lead + With Lead)
- Both sliders convert together on metric toggle
4. **`StarPilotButtonToggleControl`** - Toggle + sub-toggles
- Toggle switch with additional sub-option toggles
- Used for: CECurves, CELead, MapGears, ToyotaDoors, SLCConfirmation, PedalsOnUI
- Can be exclusive (selecting one disables others) or additive
5. **`StarPilotButtonsControl`** - Multiple action buttons
- Row of buttons (DELETE, DOWNLOAD, SELECT, etc.)
- Used for: Theme downloads, backups, model management, Weather key
- Button count varies: 2-4 buttons per control
- Can show/hide individual buttons dynamically
6. **`ButtonParamControl`** - Button selection (not value)
- Opens dialog to select from options
- Used for: AccelerationProfile, DecelerationProfile, CameraView, SLCFallback, SLCOverride
7. **`LabelControl`** - Read-only display
- Shows static text or dynamic values
- Used for: Vehicle info, Stats, Calibration values, Download status
- No user interaction
8. **`StarPilotManageControl`** - Opens sub-panel
- MANAGE/VIEW button that opens nested panel
- Used for: All main panel categories
#### Required Factory Functions:
```python
# 1. Simple value slider (StarPilotParamValueControl equivalent)
def value_item(
title: str | Callable[[], str],
value: float | Callable[[], float],
min_val: float = 0,
max_val: float = 100,
step: float = 1,
unit: str = "",
description: str | Callable[[], str] | None = None,
callback: Callable[[float], None] | None = None,
icon: str = "",
enabled: bool | Callable[[], bool] = True,
is_metric: bool = False, # Auto-convert units
labels: dict[float, str] = None, # Custom labels: {0: "Off", 1: "1 second", etc.}
negative: bool = False, # Allow negative values
) -> ListItem:
"""Slider for value adjustment with optional unit label"""
# 2. Value slider with button (StarPilotParamValueButtonControl equivalent)
def value_button_item(
title: str | Callable[[], str],
value: float | Callable[[], float],
min_val: float = 0,
max_val: float = 100,
step: float = 1,
unit: str = "",
button_text: str = "Reset", # "Reset" or "Test"
button_callback: Callable | None = None,
description: str | Callable[[], str] | None = None,
callback: Callable[[float], None] | None = None,
icon: str = "",
enabled: bool | Callable[[], bool] = True,
sub_toggles: list[str] = [], # Optional sub-toggle param keys
sub_toggle_names: list[str] = [], # Display names
has_sub_toggle: bool = False, # Has sub-toggle alongside value
labels: dict[float, str] = None,
negative: bool = False,
) -> ListItem:
"""Slider with action button (Reset/Test) and optional sub-toggles"""
# 3. Dual value control (StarPilotDualParamValueControl equivalent)
def dual_value_item(
title: str | Callable[[], str],
value1: float | Callable[[], float],
value2: float | Callable[[], float],
min_val: float = 0,
max_val: float = 100,
step: float = 1,
unit: str = "",
label1: str = "", # e.g., "Without Lead"
label2: str = "", # e.g., "With Lead"
description: str | Callable[[], str] | None = None,
callback1: Callable[[float], None] | None = None,
callback2: Callable[[float], None] | None = None,
icon: str = "",
enabled: bool | Callable[[], bool] = True,
) -> ListItem:
"""Two connected value sliders with labels"""
# 4. Button toggle control (StarPilotButtonToggleControl equivalent)
def button_toggle_item(
title: str | Callable[[], str],
state: bool | Callable[[], bool],
sub_toggles: list[str] = [], # List of sub-toggle param keys
sub_toggle_names: list[str] = [], # Display names
description: str | Callable[[], str] | None = None,
callback: Callable[[bool], None] | None = None,
sub_callbacks: list[Callable] = [],
icon: str = "",
enabled: bool | Callable[[], bool] = True,
exclusive: bool = False, # If true, selecting one disables others
) -> ListItem:
"""Toggle with optional sub-toggles (e.g., "With Lead", "Lower Limits")"""
# 5. Multi-button control (StarPilotButtonsControl equivalent)
def buttons_item(
title: str | Callable[[], str],
buttons: list[str], # e.g., ["DELETE", "DOWNLOAD", "SELECT"]
button_callbacks: list[Callable] = [],
description: str | Callable[[], str] | None = None,
icon: str = "",
enabled: bool | Callable[[], bool] = True,
initial_value: str = "", # Display current selection
) -> ListItem:
"""Multiple action buttons in a row"""
# 6. Button selection control (ButtonParamControl equivalent)
def selection_button_item(
title: str | Callable[[], str],
options: list[str], # e.g., ["Standard", "Eco", "Sport", "Sport+"]
selected_index: int = 0,
description: str | Callable[[], str] | None = None,
callback: Callable[[int, str], None] = None, # index, option_text
icon: str = "",
enabled: bool | Callable[[], bool] = True,
) -> ListItem:
"""Button that opens selection dialog"""
# 7. Label control (LabelControl equivalent) - read-only display
def label_item(
title: str | Callable[[], str],
value: str | Callable[[], str],
description: str | Callable[[], str] | None = None,
icon: str = "",
enabled: bool | Callable[[], bool] = True,
) -> ListItem:
"""Read-only label display"""
```
**Features needed across all value controls**:
- Horizontal slider with min/max/step
- Current value display with unit
- Optional "Reset" or "Test" button
- Metric unit auto-conversion (imperial/metric toggle)
- Labels update based on metric setting
- Default value display in title (e.g., "Actuator Delay (Default: 0.5)")
- Custom labels for special values (e.g., "Off", "Muted", "Auto")
- Warning labels for certain controls
### 9.4 Required: Three-Level Navigation System
Each MANAGE button in Qt leads to a sub-panel with more controls. The Longitudinal panel has **THREE levels** (Weather):
#### 9.4.1 Raylib Implementation Pattern
The Raylib implementation uses a **panel stack + callback pattern** to achieve hierarchical navigation:
**1. StarPilotLayout (Main Container):**
```python
class StarPilotLayout(Widget):
def __init__(self):
# Panel stack tracks navigation history
self._panel_stack: list[tuple[StarPilotPanelType, str]] = []
self._sub_panel_callbacks: dict[str, Callable] = {}
# Set up sub-panel navigation
self._setup_longitudinal_sub_panels()
def navigate_back(self):
# Pop from stack, update sub-panel visibility
if self._panel_stack:
self._panel_stack.pop()
self._update_sub_panel_visibility()
else:
self._set_current_panel(StarPilotPanelType.MAIN)
def _push_sub_panel(self, sub_panel_name: str):
self._panel_stack.append((self._current_panel, sub_panel_name))
self._update_sub_panel_visibility()
```
**2. StarPilotLongitudinalLayout (Sub-Panel Container):**
```python
class StarPilotLongitudinalLayout(StarPilotPanel):
def __init__(self):
super().__init__()
# Navigation callbacks and routing are handled by StarPilotPanel.__init__()
self._sub_panels: dict[str, Widget] = {
"weather": StarPilotWeatherLayout(),
"low_visibility": StarPilotLowVisibilityLayout(),
}
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(self._navigate_to)
```
**3. Weather Condition Layout (Leaf Panel):**
```python
class StarPilotWeatherLayout(StarPilotPanel):
def __init__(self):
super().__init__()
# Callbacks and state routing are inherited out of the box
items = [
button_item(
tr_noop("Low Visibility"),
lambda: tr("MANAGE"),
description,
callback=lambda: self._navigate_to("low_visibility"),
),
# ... more conditions
]
self._scroller = Scroller(items, line_separator=True, spacing=0)
```
**Key Patterns:**
1. **Navigation callback chain**: Parent → Child → Grandchild flows through callbacks
2. **State-based rendering**: Parent panel decides which sub-panel to render based on `_current_sub_panel`
3. **Back navigation**: Settings sidebar's back button pops the panel stack
4. **Separate layout classes**: Each sub-panel is its own Widget class with its own Scroller
**Navigation Flow:**
```
Driving Controls → GAS/BRAKE (Longitudinal)
→ Weather button calls _navigate_to("weather")
→ _navigate_callback pushes to panel stack
→ Longitudinal renders StarPilotWeatherLayout
→ Low Visibility button calls _navigate("low_visibility")
→ _navigate_callback pushes to panel stack
→ Longitudinal renders StarPilotLowVisibilityLayout
→ Back button pops stack → returns to Weather
```
1. **Sub-sub-panel enum**: Add `StarPilotSubPanelType` for controls within sub-panels
2. **Back button**: Each sub-panel needs back navigation to parent
3. **Navigation stack**: Track depth for proper back navigation
4. **Three-level handling**: Weather → 4 weather conditions → offset controls
**Navigation Signals in Qt**:
```cpp
// Signals for multi-level navigation
emit openSubPanel(); // Level 1 → Level 2
emit openSubSubPanel(); // Level 2 → Level 3 (personalities, SLC QOL/Offsets/Visuals)
emit openSubSubSubPanel(); // Level 3 → Level 4 (Weather conditions)
// Close signals (from parent window)
emit closeSubPanel(); // Back from Level 2 → Level 1
emit closeSubSubPanel(); // Back from Level 3 → Level 2
emit closeSubSubSubPanel(); // Back from Level 4 → Level 3
```
**Longitudinal Panel Structure** (17 sub-panels, ~80 controls):
```
STARPILOT LONGITUDINAL LAYOUT (top level - MANAGE buttons)
├── Advanced Longitudinal Tuning → StarPilotAdvancedLongLayout
│ └── 8 value controls
├── Conditional Experimental Mode → StarPilotConditionalExpLayout
│ └── 7 controls + dual slider CESpeed
├── Curve Speed Controller → StarPilotCurveSpeedLayout
│ └── 4 controls
├── Driving Personalities → StarPilotPersonalityLayout (sub-sub)
│ ├── Traffic Personality → StarPilotTrafficLayout (sub-sub-sub)
│ │ └── 7 value controls + reset
│ ├── Aggressive Personality → StarPilotAggressiveLayout
│ │ └── 7 value controls + reset
│ ├── Standard Personality → StarPilotStandardLayout
│ │ └── 7 value controls + reset
│ └── Relaxed Personality → StarPilotRelaxedLayout
│ └── 7 value controls + reset
├── Longitudinal Tuning → StarPilotLongTuneLayout
│ └── 8 controls
├── Quality of Life → StarPilotLongQOLLayout
│ └── 7 controls + Weather Presets → StarPilotWeatherLayout (LEVEL 3)
│ ├── Low Visibility → StarPilotLowVisibilityLayout (LEVEL 4)
│ │ └── 4 offset controls
│ ├── Rain → StarPilotRainLayout
│ │ └── 4 offset controls
│ ├── Rainstorm → StarPilotRainStormLayout
│ │ └── 4 offset controls
│ └── Snow → StarPilotSnowLayout
│ └── 4 offset controls
│ └── Set Weather Key (ADD/TEST buttons)
└── Speed Limit Controller → StarPilotSLCLayout
├── Main SLC Controls (~10 toggles/controls)
├── SLC Quality of Life → StarPilotSLCQOLLayout (sub-sub)
│ └── SLCConfirmation (TOGGLE+TOGGLE): Lower Limits + Higher Limits
├── SLC Offsets → StarPilotSLCOffsetsLayout (sub-sub)
│ └── 7 speed range offsets
└── SLC Visuals → StarPilotSLCVisualsLayout (sub-sub)
└── 2 toggles
```
### 9.5 Complete Panel Implementation Details
#### 9.5.1 Sounds Panel
**Qt File**: `starpilot/ui/qt/offroad/sounds_settings.cc` (222 lines)
**Raylib Implementation**: `selfdrive/ui/layouts/settings/starpilot/sounds.py`
- `StarPilotSoundsLayout`: Main panel with 2 MANAGE buttons
- `StarPilotVolumeControlLayout`: 7 volume sliders with Test buttons
- `StarPilotCustomAlertsLayout`: 5 toggles with conditional visibility
**Main Panel** (2 items):
| Control | Type | Description |
|--------|------|-------------|
| Alert Volume Controller | MANAGE | Opens sub-panel with 7 volume sliders |
| StarPilot Alerts | MANAGE | Opens sub-panel with 5 toggles |
**Alert Volume Control Sub-Panel** (7 value controls):
| Control | Range | Default | Special |
|--------|-------|---------|---------|
| Disengage Volume | 0-101 | 100 | "Test" button; 0=Muted, 101=Auto |
| Engage Volume | 0-101 | 100 | "Test" button |
| Prompt Volume | 0-101 | 100 | "Test" button |
| Prompt Distracted Volume | 0-101 | 100 | "Test" button |
| Refuse Volume | 0-101 | 100 | "Test" button |
| Warning Soft Volume | 25-101 | 100 | "Test" button; min 25 |
| Warning Immediate Volume | 25-101 | 100 | "Test" button; min 25 |
**Values**: 0=Muted, 101=Auto, 1-100=percentage
**Sound Test Implementation** (Raylib):
- **Offroad**: Persistent Python subprocess reads `path|volume\n` from stdin, plays WAV via sounddevice
- **Onroad**: Sets `TestAlert` param in params_memory (handled by `soundd.py`)
- Theme sounds checked first (`ACTIVE_THEME_PATH / "sounds"`), fallback to stock sounds
**Custom Alerts Sub-Panel** (5 toggles):
| Control | Param Key | Visibility Condition |
|--------|-----------|---------------------|
| Goat Scream | `GoatScream` | Always visible |
| Green Light Alert | `GreenLightAlert` | Always visible |
| Lead Departing Alert | `LeadDepartingAlert` | Always visible |
| Loud Blindspot Alert | `LoudBlindspotAlert` | Requires `hasBSM` |
| Speed Limit Changed Alert | `SpeedLimitChangedAlert` | Requires `ShowSpeedLimits` OR (`hasOpenpilotLongitudinal` AND `SpeedLimitController`) |
---
#### 9.5.2 Driving Model Panel
**Qt File**: `starpilot/ui/qt/offroad/model_settings.cc` (814 lines)
**Main Panel** (9 controls):
| Control | Type | Param Key | Special |
|---------|------|-----------|---------|
| AutomaticallyDownloadModels | Toggle | `AutomaticallyDownloadModels` | Auto-download new models |
| Download Driving Models | BUTTON | `DownloadModel` | DOWNLOAD / DOWNLOAD ALL / CANCEL |
| Delete Driving Models | BUTTON | `DeleteModel` | DELETE / DELETE ALL |
| Model Randomizer | Toggle | `ModelRandomizer` | Random model each drive |
| Recovery Power | VALUE+RESET | `RecoveryPower` | Range 0.5-2.0, step 0.1, Level 3 only |
| Stop Distance | VALUE+RESET | `StopDistance` | Range 4-10m, step 0.1, Level 3 only |
| Manage Blacklisted Models | BUTTON | `ManageBlacklistedModels` | ADD / REMOVE / REMOVE ALL |
| Manage Model Ratings | BUTTON | `ManageScores` | RESET / VIEW |
| Select Driving Model | BUTTON | `SelectModel` | Opens model selection dialog |
**Model Selection Dialog Features**:
- Groups models by series (Custom Series, Driving Policy, etc.)
- Shows icons: 🗺️ (Navigation), 📡 (Radar), 👀 (VOACC)
- Supports user favorites and community favorites
- Sort modes: alphabetical, release date, rating
- Shows release dates when available
- Requires reboot after selection if started
**Model Delete Dialog**:
- Groups deletable models by series
- Confirmation dialog before delete
**Blacklist Management**:
- ADD: Select from available models to blacklist
- REMOVE: Select from blacklisted models to remove
- REMOVE ALL: Clear entire blacklist (with confirmation)
**Model Ratings Panel** (Sub-panel):
- Shows each model's: Drives count, Score percentage
- Read-only LabelControls
**Conditional Visibility**:
- `ManageBlacklistedModels`, `ManageScores`: Only if `ModelRandomizer` is ON
- `SelectModel`: Only if `ModelRandomizer` is OFF
- `RecoveryPower`, `StopDistance`: Only if tuningLevel == 3 (Developer)
**Button States**:
- Download/Delete buttons: Disabled when no models available or downloading
- Online/parked requirement for downloads
---
#### 9.5.3 Lateral Panel - 5 Sub-Panels
**Qt File**: `starpilot/ui/qt/offroad/lateral_settings.cc` (428 lines)
**Advanced Lateral Tuning** (8 controls):
| Control | Type | Range | Step | Default | Special |
|---------|------|-------|------|---------|---------|
| SteerDelay | VALUE+RESET | 0.01-1.0 | 0.01 | car-specific | seconds, show default in title |
| SteerFriction | VALUE+RESET | 0-1 | 0.01 | car-specific | |
| SteerKP | VALUE+RESET | car×0.5 to car×1.5 | 0.01 | car-specific | Auto-scales to car |
| SteerLatAccel | VALUE+RESET | car×0.5 to car×1.5 | 0.01 | car-specific | Auto-scales to car |
| SteerRatio | VALUE+RESET | car×0.5 to car×1.5 | 0.01 | car-specific | Auto-scales to car |
| ForceAutoTune | Toggle | - | - | - | |
| ForceAutoTuneOff | Toggle | - | - | - | |
| ForceTorqueController | Toggle | - | - | - | |
**Car-Specific Ranges** (in showEvent):
- SteerKP: `parent->steerKp * 0.5` to `parent->steerKp * 1.5`
- SteerLatAccel: `parent->latAccelFactor * 0.5` to `parent->latAccelFactor * 1.5`
- SteerRatio: `parent->steerRatio * 0.5` to `parent->steerRatio * 1.5`
**Always On Lateral** (2 controls):
| Control | Type | Range | Step | Special |
|---------|------|-------|------|---------|
| AlwaysOnLateralLKAS | Toggle | - | - | Enable with LKAS |
| PauseAOLOnBrake | VALUE | 0-99 | 1 | mph/kmh auto-convert |
**Lane Changes** (5 controls):
| Control | Type | Range | Step | Special |
|---------|------|-------|------|---------|
| LaneChanges | MANAGE | - | - | Parent toggle |
| NudgelessLaneChange | Toggle | - | - | |
| LaneChangeTime | VALUE | 0-5 | 0.1 | seconds (labels: "Instant", "0.5 seconds", etc.) |
| MinimumLaneChangeSpeed | VALUE | 0-99 | 1 | mph/kmh |
| LaneDetectionWidth | VALUE | 0-15 | 0.1 | feet/meters (auto-convert) |
| OneLaneChange | Toggle | - | - | |
**Lateral Tuning** (3 controls):
| Control | Type | Special |
|---------|------|---------|
| TurnDesires | Toggle | |
| NNFF | Toggle | Requires NNFF log, not angle car |
| NNFFLite | Toggle | |
**Quality of Life** (1 control):
| Control | Type | Range | Step | Special |
|---------|------|-------|------|---------|
| PauseLateralSpeed | VALUE+TOGGLE | 0-99 | 1 | Has "Turn Signal Only" sub-toggle |
**Reboot Requirements** (toggle changed while started):
- AlwaysOnLateral, ForceTorqueController, NNFF, NNFFLite: All require reboot confirmation
**Metric Conversion** (updateMetric):
- LaneDetectionWidth: FOOT_TO_METER (imperial: 0-15 feet, metric: 0-5 meters)
- MinimumLaneChangeSpeed, PauseAOLOnBrake, PauseLateralSpeed: MILE_TO_KM (imperial: 0-99 mph, metric: 0-150 km/h)
**Conditional Visibility** (Advanced Lateral):
| Control | Visibility Condition |
|---------|---------------------|
| SteerDelay | Only if != 0 |
| SteerFriction | Only if !=0 AND (auto-tune off OR not forcing) AND (torque car OR forcing torque OR using NNFF) AND NOT using NNFF |
| SteerKP | Only if !=0 AND (torque car OR forcing) AND NOT angle car |
| SteerLatAccel | Only if !=0 AND (auto-tune off OR not forcing) AND (torque car OR forcing) AND NOT using NNFF |
| SteerRatio | Only if !=0 AND (auto-tune off OR not forcing) |
| ForceAutoTune | Only if NOT hasAutoTune AND NOT angle car AND (torque car OR forcingTorque OR usingNNFF) |
| ForceAutoTuneOff | Only if hasAutoTune |
| ForceTorqueController | Only if NOT angle car AND NOT torque car |
**Conditional Visibility** (Lane Changes):
- `LaneChangeTime`, `LaneDetectionWidth`: Only if LaneChanges AND NudgelessLaneChange both ON
**Conditional Visibility** (Lateral Tuning):
- `NNFF`: Only if hasNNFFLog AND NOT angle car
- `NNFFLite`: Only if NOT usingNNFF AND NOT angle car
---
#### 9.5.4 Longitudinal Panel - 17 Sub-Panels (THREE LEVELS!)
**Qt File**: `starpilot/ui/qt/offroad/longitudinal_settings.cc` (1085 lines)
**Structure**:
```
LONGITUDINAL PANEL (Main)
├── Advanced Longitudinal Tuning (8 controls)
├── Conditional Experimental Mode (7 controls + dual slider)
├── Curve Speed Controller (4 controls)
├── Driving Personalities (MANAGE) → Sub-Sub-Panel
│ ├── Traffic Personality (MANAGE) → Sub-Sub-Sub-Panel (7 controls + reset)
│ ├── Aggressive Personality (MANAGE) → Sub-Sub-Sub-Panel (7 controls + reset)
│ ├── Standard Personality (MANAGE) → Sub-Sub-Sub-Panel (7 controls + reset)
│ └── Relaxed Personality (MANAGE) → Sub-Sub-Sub-Panel (7 controls + reset)
├── Longitudinal Tuning (8 controls)
├── Quality of Life (8 controls)
│ └── Weather Presets (MANAGE) → LEVEL 3
│ ├── Low Visibility (MANAGE) → LEVEL 4 (4 offsets)
│ │ └── 4 offset controls
│ ├── Rain (MANAGE) → LEVEL 4 (4 offsets)
│ │ └── 4 offset controls
│ ├── Rainstorm (MANAGE) → LEVEL 4 (4 offsets)
│ │ └── 4 offset controls
│ └── Snow (MANAGE) → LEVEL 4 (4 offsets)
│ └── 4 offset controls
│ └── Set Weather Key (ADD/TEST buttons)
└── Speed Limit Controller (MANAGE) → Sub-Sub-Panel
├── Main SLC Controls (~10 toggles/controls)
├── SLC Quality of Life (MANAGE) → Sub-Sub-Sub-Panel
│ └── SLCConfirmation (TOGGLE+TOGGLE): Lower Limits + Higher Limits
├── SLC Offsets (MANAGE) → Sub-Sub-Sub-Panel (7 speed range offsets)
└── SLC Visuals (MANAGE) → Sub-Sub-Sub-Panel (2 toggles)
```
**Advanced Longitudinal Tuning** (8 controls):
| Control | Type | Range | Step | Unit | Special |
|---------|------|-------|------|------|---------|
| LongitudinalActuatorDelay | VALUE | 0-1 | 0.01 | seconds | show default in title |
| MaxDesiredAcceleration | VALUE | 0.1-4.0 | 0.1 | m/s² | |
| StartAccel | VALUE | 0-4 | 0.01 | m/s² | show default in title |
| VEgoStarting | VALUE | 0.01-1 | 0.01 | m/s² | show default in title |
| StopAccel | VALUE | -4-0 | 0.01 | m/s² | negative, show default |
| StoppingDecelRate | VALUE | 0.001-1 | 0.001 | m/s² | show default |
| VEgoStopping | VALUE | 0.01-1 | 0.01 | m/s² | show default |
**Conditional Experimental Mode** (8 controls - includes dual slider):
| Control | Type | Range | Step | Special |
|---------|------|-------|------|---------|
| CESpeed | DUAL VALUE | 0-99 (0-150 metric) | 1 | TWO sliders: Without Lead + With Lead |
| CECurves | TOGGLE+TOGGLE | - | - | Main + "With Lead" sub-toggle |
| CEStopLights | Toggle | - | - | "Detected" stop lights/signs |
| CELead | TOGGLE+TOGGLE | - | - | Main + "Slower Lead" + "Stopped Lead" |
| CEModelStopTime | VALUE | 0-9 | 1 | seconds (labels: "Off", "1 second", etc.) |
| CESignalSpeed | VALUE+TOGGLE | 0-99 (0-150 metric) | 1 | mph/kmh + "Not For Detected Lanes" |
| ShowCEMStatus | Toggle | - | - | Status widget |
**Dual Slider Implementation**:
- Uses `StarPilotDualParamValueControl` wrapping two `StarPilotParamValueControl`
- CESpeed: First slider "Below" (no lead), Second slider "With Lead"
- Both sliders share same unit and get converted together
**Curve Speed Controller** (4 controls):
| Control | Type | Special |
|---------|------|---------|
| CalibratedLateralAcceleration | LABEL | Read-only, shows "X.XX m/s²" |
| CalibrationProgress | LABEL | Read-only, shows "XX.XX%" |
| ResetCurveData | BUTTON | RESET |
| ShowCSCStatus | Toggle | Status widget |
**Driving Personalities** (4 sub-panels, each 7 controls + reset):
*Each personality has*:
- Follow: 0.5-3s (Traffic) or 1-3s (others), step 0.01
- JerkAcceleration: 25-200%, step 1
- JerkDeceleration: 25-200%, step 1
- JerkDanger: 25-200%, step 1
- JerkSpeedDecrease: 25-200%, step 1
- JerkSpeed: 25-200%, step 1
- Reset button
| Personality | Follow Range | Default Follow | Icon |
|------------|--------------|----------------|------|
| Traffic | 0.5-3.0 | N/A | traffic.png |
| Aggressive | 1-3 | 1.25s | aggressive.png |
| Standard | 1-3 | 1.45s | standard.png |
| Relaxed | 1-3 | 1.75s | relaxed.png |
**Longitudinal Tuning** (8 controls):
| Control | Type | Options | Special |
|---------|------|---------|---------|
| AccelerationProfile | BUTTON SELECT | Standard, Eco, Sport, Sport+ | |
| DecelerationProfile | BUTTON SELECT | Standard, Eco, Sport | |
| HumanAcceleration | Toggle | | |
| HumanFollowing | Toggle | | |
| HumanLaneChanges | Toggle | | Requires radar |
| LeadDetectionThreshold | VALUE | 25-50% | |
| TacoTune | Toggle | | "Taco Bell Run" turn speed |
| MapGears | TOGGLE+TOGGLE | | Acceleration + Deceleration mapped to gears |
**Quality of Life** (8 controls):
| Control | Type | Range | Step | Unit | Special |
|---------|------|-------|------|------|---------|
| CustomCruise | VALUE | 1-99 | 1 | mph/kmh | Cruise interval |
| CustomCruiseLong | VALUE | 1-99 | 1 | mph/kmh | Cruise interval (hold) |
| ForceStops | Toggle | - | - | | Force stop at detected lights |
| IncreasedStoppedDistance | VALUE | 0-10 | 1 | feet/meters | |
| MapGears | TOGGLE+TOGGLE | - | - | See LongitudinalTune |
| SetSpeedOffset | VALUE | 0-99 | 1 | mph/kmh | |
| ReverseCruise | Toggle | - | - | Reverse +/- behavior |
| WeatherPresets | MANAGE | - | - | Opens Weather panel (LEVEL 3) |
**Weather** (4 conditions × 4 controls = 16 controls) - LEVEL 3:
| Condition | Controls (each) | Range |
|----------|----------------|-------|
| Low Visibility | IncreaseFollowing, IncreasedStoppedDistance, ReduceAcceleration, ReduceLateralAccel | Following: 0-3s, Stopped: 0-10ft, Accel: 0-99%, Lateral: 0-99% |
| Rain | Same 4 offsets | Same |
| Rainstorm | Same 4 offsets | Same |
| Snow | Same 4 offsets | Same |
**Weather Key Management**:
- ADD button: Opens InputDialog for 32-character key
- TEST button: Makes network request to OpenWeatherMap API
- Shows "Testing..." while request in progress
- Validates both v2.5 and v3.0 API
**Speed Limit Controller** (Main + 3 sub-panels):
*Main SLC* (10+ controls):
| Control | Type | Options | Special |
|---------|------|---------|---------|
| SLCFallback | BUTTON SELECT | Set Speed, Experimental Mode, Previous Limit | |
| SLCOverride | BUTTON SELECT | None, Set With Gas Pedal, Max Set Speed | |
| SLCQOL | MANAGE | | Opens QOL sub-panel |
| SLCConfirmation | TOGGLE+TOGGLE | | Lower Limits + Higher Limits |
| SLCLookaheadHigher | VALUE | 0-30 | seconds |
| SLCLookaheadLower | VALUE | 0-30 | seconds |
| SetSpeedLimit | Toggle | | Match speed limit on engage |
| SLCMapboxFiller | Toggle | | Use Mapbox as fallback |
| SLCPriority | BUTTON | SELECT | Opens priority dialog |
| SLCOffsets | MANAGE | | Opens Offsets sub-panel |
*SLC Priority Dialog*:
- Two-step selection: Primary then Secondary
- Options: Dashboard (if hasDashSpeedLimits), Map Data, Highest, Lowest, None
- Stores as SLCPriority1, SLCPriority2
*SLC Offsets Sub-Panel* (7 controls):
| Control | Imperial Range | Metric Range | Special |
|---------|----------------|--------------|---------|
| Offset1 | -99-99 mph (0-24 mph range) | -150-150 km/h (0-29 km/h range) | |
| Offset2 | -99-99 mph (25-34 mph range) | -150-150 km/h (30-49 km/h range) | |
| Offset3 | -99-99 mph (35-44 mph range) | -150-150 km/h (50-59 km/h range) | |
| Offset4 | -99-99 mph (45-54 mph range) | -150-150 km/h (60-79 km/h range) | |
| Offset5 | -99-99 mph (55-64 mph range) | -150-150 km/h (80-99 km/h range) | |
| Offset6 | -99-99 mph (65-74 mph range) | -150-150 km/h (100-119 km/h range) | |
| Offset7 | -99-99 mph (75-99 mph range) | -150-150 km/h (120-140 km/h range) | |
*SLC Visuals Sub-Panel* (2 controls):
| Control | Type |
|---------|------|
| ShowSLCOffset | Toggle |
| SpeedLimitSources | Toggle |
**Metric Conversion Constants**:
```cpp
FOOT_TO_METER = 0.3048
METER_TO_FOOT = 3.28084
MILE_TO_KM = 1.60934
KM_TO_MILE = 0.621371
```
**Conditional Visibility**:
- `CEStopLights`: Only if tuningLevel < CEModelStopTime level
- `CustomCruise`, `CustomCruiseLong`, `SetSpeedLimit`, `SetSpeedOffset`: Only if NOT hasPCMCruise
- `HumanLaneChanges`: Only if hasRadar
- `MapGears`: Only if isToyota AND NOT isTSK
- `ReverseCruise`: Only if isToyota
- `SLCMapboxFiller`: Only if MapboxSecretKey is set
- `StartAccel`: Only if NOT (LongitudinalTune AND HumanAcceleration)
- `StoppingDecelRate`, `VEgoStarting`, `VEgoStopping`: Only if NOT (isGM AND ExperimentalGMTune) AND NOT (isToyota AND FrogsGoMoosTweak)
---
#### 9.5.5 Visual Panel - 5 Sub-Panels
**Qt File**: `starpilot/ui/qt/offroad/visual_settings.cc` (328 lines)
**Advanced Custom UI** (6 toggles):
| Control | Param Key |
|---------|-----------|
| HideSpeed | `HideSpeed` |
| HideLeadMarker | `HideLeadMarker` |
| HideMaxSpeed | `HideMaxSpeed` |
| HideAlerts | `HideAlerts` |
| HideSpeedLimit | `HideSpeedLimit` |
| WheelSpeed | `WheelSpeed` |
**Driving Screen Widgets** (7 controls):
| Control | Type | Special |
|---------|------|---------|
| AccelerationPath | Toggle | Requires longitudinal |
| AdjacentPath | Toggle | |
| BlindSpotPath | Toggle | Requires BSM |
| Compass | Toggle | |
| OnroadDistanceButton | Toggle | Requires longitudinal |
| PedalsOnUI | TOGGLE+TOGGLE | Dynamic + Static sub-toggles (exclusive) |
| RotatingWheel | Toggle | |
**PedalsOnUI Sub-Toggles**:
- Dynamic: When selected, disables Static
- Static: When selected, disables Dynamic
- Uses special exclusive toggle behavior
**Model UI** (5 value controls):
| Control | Range | Step | Unit | Metric Range | Special |
|---------|-------|------|------|--------------|---------|
| DynamicPathWidth | 0-100 | 1 | % | same | |
| LaneLinesWidth | 0-24 | 1 | inches | 0-60 cm | Auto-convert to cm |
| PathEdgeWidth | 0-100 | 1 | % | same | |
| PathWidth | 0-10 | 0.1 | feet | 0-3 m | Auto-convert to meters |
| RoadEdgesWidth | 0-24 | 1 | inches | 0-60 cm | Auto-convert to cm |
**PathEdgeWidth Labels**: "Off", "1%" through "100%"
**Navigation UI** (4 toggles):
| Control | Param Key |
|---------|-----------|
| RoadNameUI | `RoadNameUI` |
| ShowSpeedLimits | `ShowSpeedLimits` |
| SLCMapboxFiller | `SLCMapboxFiller` |
| UseVienna | `UseVienna` |
**Quality of Life** (4 controls):
| Control | Type | Options | Special |
|---------|------|---------|---------|
| CameraView | BUTTON SELECT | Auto, Driver, Standard, Wide | |
| DriverCamera | Toggle | | Show in reverse |
| StoppedTimer | Toggle | | |
**Metric Conversion**:
- LaneLinesWidth, RoadEdgesWidth: INCH_TO_CM / CM_TO_INCH (imperial: 0-24 inches, metric: 0-60 cm)
- PathWidth: FOOT_TO_METER (imperial: 0-10 feet, metric: 0-3 meters)
**Conditional Visibility**:
- `AccelerationPath`, `OnroadDistanceButton`, `PedalsOnUI`, `HideLeadMarker`: Only if hasOpenpilotLongitudinal
- `BlindSpotPath`: Only if hasBSM
- `HideSpeedLimit`: Only if hasOpenpilotLongitudinal AND SpeedLimitController
- `ShowSpeedLimits`: Only if NOT SpeedLimitController OR NOT hasOpenpilotLongitudinal
- `SLCMapboxFiller`: Only if ShowSpeedLimits AND (NOT SpeedLimitController OR NOT hasOpenpilotLongitudinal) AND MapboxSecretKey set
- `UseVienna`: Only if ShowSpeedLimits OR SpeedLimitController
---
#### 9.5.6 Themes Panel
**Qt File**: `starpilot/ui/qt/offroad/theme_settings.cc` (935 lines)
**Main Panel**:
| Control | Type | Buttons | Special |
|---------|------|---------|---------|
| Custom Themes | MANAGE | - | Opens 7 theme types |
| Holiday Themes | TOGGLE | - | |
| Rainbow Path | Toggle | | |
| Random Events | Toggle | | |
| Random Themes | TOGGLE+TOGGLE | - | Includes "Include Holiday Themes" |
| Startup Alert | BUTTON | STOCK, STARPILOT, CUSTOM, CLEAR | |
| Download Status | LABEL | - | Shows download progress |
**Custom Themes Sub-Panel** (7 theme types, each with DELETE/DOWNLOAD/SELECT):
| Theme Type | Param Key | Buttons | Directory |
|------------|-----------|---------|-----------|
| Boot Logo | `BootLogo` | DELETE, DOWNLOAD, SELECT | boot_logos/ |
| Color Scheme | `ColorScheme` | DELETE, DOWNLOAD, SELECT | colors/ |
| Distance Icon | `DistanceIconPack` | DELETE, DOWNLOAD, SELECT | distance_icons/ |
| Icon Pack | `IconPack` | DELETE, DOWNLOAD, SELECT | (root level) |
| Signal Animation | `SignalAnimation` | DELETE, DOWNLOAD, SELECT | signal_animations/ |
| Sound Pack | `SoundPack` | DELETE, DOWNLOAD, SELECT | sounds/ |
| Wheel Icon | `WheelIcon` | DELETE, DOWNLOAD, SELECT | steering_wheels/ |
**Theme Button Behaviors**:
- DELETE: Opens selection dialog, then confirmation dialog
- DOWNLOAD: Opens selection dialog, starts download, shows progress
- SELECT: Opens selection dialog (includes "Stock" + holidays for Color/Distance)
**Theme Download States**:
- Shows "Downloading..." during download
- Buttons disabled while downloading
- CANCEL available during download
**Holiday Themes** (13 holidays):
- New Year's, Valentine's Day, St. Patrick's Day, World Frog Day, April Fools, Easter, May the Fourth, Cinco de Mayo, Stitch Day, Fourth of July, Halloween, Thanksgiving, Christmas
**Random Themes Behavior**:
- Pick random theme between each drive
- Optionally include holiday themes
**Theme Name Parsing**:
- Format: "name~creator" (stored lowercase)
- User-created themes: "name-user_created"
- Display with 🌟 for user-created, "- by: creator" for community
---
#### 9.5.7 Vehicle Panel - 5 Sub-Panels
**Qt File**: `starpilot/ui/qt/offroad/vehicle_settings.cc` (467 lines)
**Main Panel** (3 controls + 5 sub-panel buttons):
| Control | Type | Special |
|---------|------|---------|
| Car Make | BUTTON SELECT | 28 car makes |
| Car Model | BUTTON SELECT | Dynamic based on make |
| ForceFingerprint | Toggle | Disable auto fingerprint |
| DisableOpenpilotLongitudinal | Toggle | Use stock ACC, requires reboot |
**Car Makes** (28 total):
Acura, Audi, Buick, Cadillac, Chevrolet, Chrysler, CUPRA, Dodge, Ford, Genesis, GMC, Holden, Honda, Hyundai, Jeep, Kia, Lexus, Lincoln, MAN, Mazda, Nissan, Peugeot, Ram, Rivian, SEAT, Škoda, Subaru, Tesla, Toyota, Volkswagen
**Car Model Selection**:
- Reads from opendbc/car/{folder}/values.py
- Groups by platform (e.g., "Toyota", "Genesis", etc.)
**Sub-Panel Buttons** (MANAGE/VIEW):
- GM Settings, HKG Settings, Subaru Settings, Toyota Settings, Vehicle Info
**GM Sub-Panel** (4 controls):
| Control | Type | Special |
|---------|------|---------|
| GMPedalLongitudinal | Toggle | Requires pedal |
| RemoteStartBootsComma | Toggle | Flash panda firmware |
| RemapCancelToDistance | Toggle | Requires BOLT + pedal |
| VoltSNG | Toggle | |
**Remote Start Flow**:
- Toggle triggers confirmation
- Shows prompt about Panda firmware update
- Runs flash in background thread
- Reboots after flash complete
**HKG Sub-Panel** (1 control):
| Control | Type |
|---------|------|
| TacoTuneHacks | Toggle |
**Subaru Sub-Panel** (1 control):
| Control | Type |
|---------|------|
| SubaruSNG | Toggle |
**Toyota Sub-Panel** (5 controls):
| Control | Type | Range | Step | Special |
|---------|------|-------|------|---------|
| ToyotaDoors | TOGGLE+TOGGLE | - | - | Lock + Unlock |
| ClusterOffset | VALUE+RESET | 1.000-1.050 | 0.001 | x multiplier |
| FrogsGoMoosTweak | Toggle | | | |
| LockDoorsTimer | VALUE | 0-300 | 5 | seconds, WARNING label |
| SNGHack | Toggle | | | |
**ClusterOffset**:
- Shows "x1.000" to "x1.050"
- Reset to default 1.000
- Displayed as multiplication factor
**LockDoorsTimer Labels**:
- 0 = "Never"
- 1-300 = "X seconds"
**Warning Label**:
- LockDoorsTimer: "<b>Warning:</b> openpilot can't detect if keys are still inside the car..."
**Vehicle Info Sub-Panel** (7 read-only labels):
| Control | Value |
|---------|-------|
| Hardware Detected | "None" or "comma Pedal, SDSU, ZSS" |
| Blind Spot Support | "Yes" / "No" |
| Pedal Support | "Yes" / "No" |
| openpilot Longitudinal | "Yes" / "No" |
| Radar Support | "Yes" / "No" |
| SDSU Support | "Yes" / "No" |
| Stop-and-Go Support | "Yes" / "No" |
**Reboot Requirements**:
- TacoTuneHacks: Requires reboot
- RemapCancelToDistance: Requires reboot
- DisableOpenpilotLongitudinal: Requires reboot if disabling while started
**Conditional Visibility**:
- `GMPedalLongitudinal`: Only if hasPedal OR (PC and canUsePedal)
- `RemapCancelToDistance`: Only if isBolt AND (hasPedal OR (PC and canUsePedal))
- `SubaruSNG`: Only if hasSNG
- `TacoTuneHacks`: Only if isHKGCanFd
- `VoltSNG`: Only if isVolt AND NOT hasSNG
- `SNGHack`: Only if NOT hasSNG
---
#### 9.5.8 Wheel Panel
**Qt File**: `starpilot/ui/qt/offroad/wheel_settings.cc` (84 lines)
**4 Controls** (each opens selection dialog):
| Control | Param Key | Options |
|---------|-----------|---------|
| Distance Button | `DistanceButtonControl` | No Action, Change Personality, Force Coast, Pause Accel/Brake, Toggle Experimental, Toggle Traffic Mode |
| Distance Button (Long Press) | `LongDistanceButtonControl` | Same + Pause Steering |
| Distance Button (Very Long Press) | `VeryLongDistanceButtonControl` | Same |
| LKAS Button | `LKASButtonControl` | No Action, Pause Steering (unless Subaru or LKAS allowed for AOL) |
**Conditional Visibility**:
- `LKASButtonControl`: NOT Subaru AND NOT (LKAS allowed for AOL AND AlwaysOnLateral AND AlwaysOnLateralLKAS)
---
#### 9.5.9 Navigation Panel
**Qt File**: `starpilot/ui/qt/offroad/navigation_settings.cc` (330 lines)
**Main Panel**:
| Control | Type | Special |
|---------|------|---------|
| Manage Your Settings At | LABEL | Shows "IP:8082" or "Offline..." |
| Public Mapbox Key | BUTTON | ADD/TEST |
| Secret Mapbox Key | BUTTON | ADD/TEST |
| Mapbox Setup Instructions | VIEW | Opens instructions sub-panel |
| Speed Limit Filler | BUTTON TOGGLE | CANCEL / Manually Update Speed Limits |
**Key Management**:
- ADD: Opens InputDialog for key entry
- TEST: Validates key works
- Keys stored in params (Public/Secret Mapbox)
**Instructions Sub-Panel**:
- Displays setup images based on current step
- Step-based instruction flow
---
#### 9.5.10 Data Panel
**Qt File**: `starpilot/ui/qt/offroad/data_settings.cc` (872 lines)
**Main Panel**:
| Control | Type | Special |
|---------|------|---------|
| Delete Driving Data | BUTTON | DELETE |
| Delete Error Logs | BUTTON | DELETE |
| Screen Recordings | BUTTONS | DELETE / DELETE ALL / RENAME |
| StarPilot Backups | BUTTONS | BACKUP / DELETE / DELETE ALL / RESTORE |
| Toggle Backups | BUTTONS | BACKUP / DELETE / DELETE ALL / RESTORE |
| StarPilot Stats | BUTTONS | RESET / VIEW |
**Stats Sub-Panel** (20+ read-only labels):
- Drives, Distance, Time, Events, Time percentages, etc.
---
#### 9.5.11 Device Panel - 2 Sub-Panels
**Qt File**: `starpilot/ui/qt/offroad/device_settings.cc` (247 lines)
**Device Management Sub-Panel** (7 controls):
| Control | Type | Range | Step | Unit | Special |
|---------|------|-------|------|------|---------|
| DeviceShutdown | VALUE | 0-33 | 1 | - | Labels: 0=5min, 1-3=15-45min, 4+=hours |
| NoLogging | Toggle | - | - | WARNING toggle |
| NoUploads | TOGGLE+TOGGLE | - | - | Main + "Disable Onroad Only" |
| HigherBitrate | Toggle | - | - | |
| LowVoltageShutdown | VALUE | 11.8-12.5 | 0.1 | volts | |
| IncreaseThermalLimits | Toggle | - | - | WARNING toggle |
| UseKonikServer | Toggle | - | - | |
**DeviceShutdown Labels**:
- 0 = "5 minutes"
- 1-3 = "15/30/45 minutes"
- 4+ = "X hours" (4=4h, 5=5h, etc.)
**Screen Management Sub-Panel** (7 controls):
| Control | Type | Range | Step | Special |
|---------|------|-------|------|---------|
| ScreenBrightness | VALUE | 1-101 | 1 | 0=Off, 101=Auto |
| ScreenBrightnessOnroad | VALUE | 0-101 | 1 | 0=Off, 101=Auto |
| ScreenRecorder | BUTTON TOGGLE | - | - | START/STOP recording |
| ScreenTimeout | VALUE | 5-60 | 5 | seconds |
| ScreenTimeoutOnroad | VALUE | 5-60 | 5 | seconds |
| StandbyMode | Toggle | - | - | |
**Screen Brightness Labels**:
- 0 = "Off"
- 1-100 = "X%"
- 101 = "Auto"
---
#### 9.5.12 Utilities Panel
**Qt File**: `starpilot/ui/qt/offroad/utilities.cc` (369 lines)
**Main Panel** (6 controls):
| Control | Type | Special |
|---------|------|---------|
| Debug Mode | Toggle | |
| Flash Panda | BUTTON | FLASH |
| Force Drive State | BUTTONS | OFFROAD / ONROAD / OFF |
| Galaxy | BUTTON | PAIR/UNPAIR |
| Report a Bug | BUTTON | REPORT |
| Reset Toggles to Default | BUTTON | RESET |
| Reset Toggles to Stock | BUTTON | RESET |
---
#### 9.5.13 Maps Panel
**Qt File**: `starpilot/ui/qt/offroad/maps_settings.cc` (278 lines)
**Main Panel**:
| Control | Type | Special |
|---------|------|---------|
| Automatically Update Maps | BUTTON SELECT | Manually/Weekly/Monthly |
| Download Maps | BUTTON | DOWNLOAD/CANCEL |
| Last Updated | LABEL | Shows date or "Never" |
| Map Sources | BUTTONS | COUNTRIES / STATES |
| Progress | LABEL | Shows during download |
| Time Elapsed | LABEL | |
| Time Remaining | LABEL | |
| Remove Maps | BUTTON | REMOVE |
| Storage Used | LABEL | Shows MB |
**Countries Sub-Panel**: 7 continents with checkbox lists
**States Sub-Panel**: 5 US regions with checkbox lists
### 9.6 Conditional Visibility Logic
Qt implements complex visibility rules based on multiple factors:
#### Metric Conversion Constants (from Qt):
```cpp
// Distance conversions
FOOT_TO_METER = 0.3048
METER_TO_FOOT = 3.28084
INCH_TO_CM = 2.54
CM_TO_INCH = 0.393701
// Speed conversions
MILE_TO_KM = 1.60934
KM_TO_MILE = 0.621371
```
#### Value Range Conversion by Unit Type:
| Unit Type | Imperial Range | Metric Range | Conversion |
|-----------|---------------|--------------|------------|
| Speed (mph/kmh) | 0-99 | 0-150 | MILE_TO_KM |
| Speed (mph/kmh) offset | -99 to 99 | -150 to 150 | MILE_TO_KM |
| Distance (feet) | 0-10 | 0-3 | FOOT_TO_METER |
| Distance (feet) offset | 0-10 | 0-3 | FOOT_TO_METER |
| Small distance (inches) | 0-24 | 0-60 | INCH_TO_CM |
| Lane width (feet) | 0-15 | 0-5 | FOOT_TO_METER |
#### Special Value Labels:
```cpp
// Volume (0-101)
0 = "Muted"
1-100 = "X%"
101 = "Auto"
// Device shutdown (0-33)
0 = "5 minutes"
1 = "15 minutes"
2 = "30 minutes"
3 = "45 minutes"
4+ = "X hours"
// Speed/time values
0 = "Off" (for many controls)
0 = "Instant" (for LaneChangeTime)
```
#### Visibility Factors in Qt:
1. **Tuning Level** (0-3): Base visibility threshold
2. **Car Type**: isGM, isHKG, isToyota, isSubaru, isAngleCar, isTorqueCar, isVolt, isBolt, isTSK, isHKGCanFd
3. **Car Capabilities**: hasBSM, hasRadar, hasPedal, hasSNG, hasNNFFLog, hasAutoTune, hasDashSpeedLimits, hasZSS, canUsePedal, canUseSDSU
4. **Feature Flags**: Other toggles states (LaneChanges, NudgelessLaneChange, ModelRandomizer, etc.)
5. **Longitudinal Control**: hasOpenpilotLongitudinal, hasPCMCruise
6. **Device State**: started (driving), parked, online, isFrogsGoMoo
7. **Special Conditions**: hasOpenpilotLongitudinalControlDisabled, hasAlphaLongitudinal
#### Additional Car State Properties Needed:
```python
class StarPilotCarState:
# Car detection
isGM: bool = False
isHKG: bool = False # Hyundai/Kia/Genesis
isToyota: bool = False
isSubaru: bool = False
isVolt: bool = False # Specific GM model
isBolt: bool = False # Specific GM model
isAngleCar: bool = False
isTorqueCar: bool = False
isTSK: bool = False # Toyota Safety Connect
isHKGCanFd: bool = False
# Capabilities
hasBSM: bool = False # Blind spot monitoring
hasRadar: bool = False
hasPedal: bool = False
hasSNG: bool = False # Stop and Go
hasNNFFLog: bool = False
hasAutoTune: bool = False
hasOpenpilotLongitudinal: bool = False
hasDashSpeedLimits: bool = False
hasZSS: bool = False # ZSS steering sensor
canUsePedal: bool = False
canUseSDSU: bool = False
# Device/car state
isFrogsGoMoo: bool = False
hasPCMCruise: bool = False
lkasAllowedForAOL: bool = False
openpilotLongitudinalControlDisabled: bool = False
hasAlphaLongitudinal: bool = False
# Car values for range calculations
steerActuatorDelay: float = 0.0
friction: float = 0.0
steerKp: float = 0.0
latAccelFactor: float = 0.0
steerRatio: float = 0.0
longitudinalActuatorDelay: float = 0.0
startAccel: float = 0.0
stopAccel: float = 0.0
stoppingDecelRate: float = 0.0
vEgoStarting: float = 0.0
vEgoStopping: float = 0.0
```
#### Examples from Qt:
**From lateral_settings.cc**:
```python
def updateToggles(self):
for key, toggle in toggles.items():
min_level = parent.starpilotToggleLevels[key]
visible = parent.tuningLevel >= min_level.toDouble()
# AlwaysOnLateralLKAS
if key == "AlwaysOnLateralLKAS":
visible &= parent.lkasAllowedForAOL
# ForceAutoTune
elif key == "ForceAutoTune":
visible &= not parent.hasAutoTune
visible &= not parent.isAngleCar
visible &= parent.isTorqueCar or forcingTorqueController or usingNNFF
# NNFF
elif key == "NNFF":
visible &= parent.hasNNFFLog
visible &= not parent.isAngleCar
# LaneChangeTime
elif key == "LaneChangeTime":
visible &= params.getBool("LaneChanges")
visible &= params.getBool("NudgelessLaneChange")
# SteerKP
elif key == "SteerKP":
visible &= parent.steerKp != 0
visible &= parent.isTorqueCar or forcingTorqueController or usingNNFF
visible &= not parent.isAngleCar
toggle.setVisible(visible)
```
**From longitudinal_settings.cc**:
```python
# Conditional Experimental Mode visibility
if key == "CEStopLights":
visible &= parent.tuningLevel < parent.starpilotToggleLevels["CEModelStopTime"].toDouble()
# Cruise controls
elif key in ["CustomCruise", "CustomCruiseLong", "SetSpeedLimit", "SetSpeedOffset"]:
visible &= not parent.hasPCMD
# MapGears
elif key == "MapGears":
visible &= parent.isToyota
visible &= not parent.isTSK
# HumanLaneChanges
elif key == "HumanLaneChanges":
visible &= parent.hasRadar
# StartAccel
elif key == "StartAccel":
visible &= not (params.getBool("LongitudinalTune") and params.getBool("HumanAcceleration"))
```
**From visual_settings.cc**:
```python
if key == "AccelerationPath":
visible &= parent.hasOpenpilotLongitudinal
elif key == "BlindSpotPath":
visible &= parent.hasBSM
elif key == "PedalsOnUI":
visible &= parent.hasOpenpilotLongitudinal
elif key == "HideSpeedLimit":
visible &= parent.hasOpenpilotLongitudinal AND SpeedLimitController
elif key == "ShowSpeedLimits":
visible &= not params.getBool("SpeedLimitController") or not parent.hasOpenpilotLongitudinal
elif key == "SLCMapboxFiller":
visible &= params.getBool("ShowSpeedLimits")
visible &= not params.getBool("SpeedLimitController") or not parent.hasOpenpilotLongitudinal
visible &= not params.get("MapboxSecretKey").empty()
```
**From vehicle_settings.cc**:
```python
if key in gmKeys:
visible &= parent.isGM
elif key in hkgKeys:
visible &= parent.isHKG
elif key in subaruKeys:
visible &= parent.isSubaru
elif key in toyotaKeys:
visible &= parent.isToyota
# Specific conditions
if key == "SNGHack":
visible &= not parent.hasSNG
elif key == "GMPedalLongitudinal":
visible &= parent.hasPedal or (Hardware.PC() and parent.canUsePedal)
elif key == "RemapCancelToDistance":
visible &= parent.isBolt and (parent.hasPedal or (Hardware.PC() and parent.canUsePedal))
```
---
### 9.7 Onroad Overlays (Priority 4)
| Feature | Qt File | Implementation |
|---------|---------|----------------|
| Blind spot visualization | `starpilot_onroad.cc` | Read BSM from car state, draw indicator |
| FPS counter overlay | `hud_renderer.py` | Already partial, add toggle |
| Steering torque | `hud_renderer.py` | Read from car state, display value |
| Turn signals | `hud_renderer.py` | Read signal state, show indicator |
---
### 9.8 Developer Sidebar (Priority 5)
**File**: `selfdrive/ui/layouts/sidebar.py`
Add toggle in sidebar for developer metrics. Show additional metrics when enabled (CPU, memory, etc. with more detail).
---
### 9.9 Implementation Order
1. **Phase 1**: ✅ Add value factory functions to list_view.py (7 types done!)
2. **Phase 2**: ✅ Implement three-level navigation system (sub-sub-sub-panels for Weather)
3. **Phase 3**: ✅ Create StarPilotState singleton (car state parsing + desktop fallback)
4. **Phase 4**: ✅ Create InputDialog & SelectionDialog widgets
5. **Phase 5**: ✅ Extend Params with get_int/get_float/put_int/put_float
6. **Phase 6**: ✅ Implement Lateral 5 sub-panels (~21 controls, car-specific ranges, conditional visibility)
7. **Phase 7**: ✅ Implement Weather sub-panels with real value_item controls + put_int callbacks
8. **Phase 8**: ✅ Complete Sounds panel (7 volume sliders + Test buttons + sub-panel + custom alerts with conditional visibility)
9. **Phase 9**: Complete Driving Model (model selection dialog, blacklist, ratings)
10. **Phase 10**: Implement Longitudinal remaining sub-panels (~60+ controls)
11. **Phase 11**: Complete Visual 5 sub-panels (~35 controls)
12. **Phase 12**: Complete Themes (7 theme types, download/delete/select buttons)
13. **Phase 13**: Complete Vehicle 5 sub-panels (~20 controls, car make/model selection)
14. **Phase 14**: Complete remaining panels (Device, Navigation, Data, Maps, Utilities, Wheel)
15. **Phase 15**: Add conditional visibility logic to ALL panels (Lateral done, rest pending)
16. **Phase 16**: Implement real button actions (download, delete, reset, dialogs)
17. **Phase 17**: Add metric unit conversion support (constants and label updates)
18. **Phase 18**: Onroad overlays (4 features)
19. **Phase 19**: Developer sidebar toggle
---
### 9.10 Qt Reference Files Summary
| Panel | Qt File | Lines | Sub-Panels | Controls | Special Features |
|-------|---------|-------|------------|---------|------------------|
| Sounds | `sounds_settings.cc` | 222 | 2 | 14+ | Test buttons, sub-panel navigation |
| Model | `model_settings.cc` | 814 | 2 | 9+ | Download/delete dialogs, model selection |
| Lateral | `lateral_settings.cc` | 428 | 5 | ~25 | Car-specific ranges, metric conversion |
| Longitudinal | `longitudinal_settings.cc` | 1085 | 17 | ~100+ | THREE-LEVEL NAV, dual sliders, weather |
| Visual | `visual_settings.cc` | 328 | 5 | ~35 | Auto unit conversion |
| Themes | `theme_settings.cc` | 935 | 1 | ~50+ | Download/select for 7 types |
| Navigation | `navigation_settings.cc` | 330 | 1 | 5+ | Key management, setup instructions |
| Data | `data_settings.cc` | 872 | 1 | 6+ | Backup/restore, stats viewing |
| Device | `device_settings.cc` | 247 | 2 | ~14 | Screen recording |
| Vehicle | `vehicle_settings.cc` | 467 | 5 | ~20 | Car-specific visibility |
| Wheel | `wheel_settings.cc` | 84 | 0 | 4 | Dynamic options based on car |
| Utilities | `utilities.cc` | 369 | 0 | 7 | Network pairing, reporting |
| Maps | `maps_settings.cc` | 278 | 2 | 9+ | Country/state selection |
| **TOTAL** | | **~5559** | **43** | **~350+** | |
---
### 9.11 Key Qt Classes to Port
| Qt Class | Purpose | Where Used |
|----------|---------|------------|
| `StarPilotParamValueControl` | Simple value slider | Lateral, Longitudinal, Visual, Device |
| `StarPilotParamValueButtonControl` | Value + Reset/Test button | Sounds, Model, Lateral, Vehicle |
| `StarPilotDualParamValueControl` | Two connected sliders | Longitudinal (CESpeed) |
| `StarPilotButtonToggleControl` | Toggle + sub-toggles | Longitudinal, Visual, Vehicle |
| `StarPilotButtonsControl` | Multiple action buttons | Themes, Data, Device, Model |
| `ButtonParamControl` | Option selection button | Longitudinal (profiles), Visual (camera) |
| `StarPilotManageControl` | Opens sub-panel | All main panels |
| `LabelControl` | Read-only display | Device info, Stats, Calibration |
| `MapSelectionControl` | Checkbox list | Maps panel |
---
### 9.12 Special UI Behaviors
#### Reboot Requirements
Some toggles require a system reboot when changed while driving:
```cpp
// Toggles that require reboot
QSet<QString> rebootKeys = {
"AlwaysOnLateral", // Lateral
"ForceTorqueController", // Lateral
"NNFF", // Lateral
"NNFFLite", // Lateral
"DisableOpenpilotLongitudinal", // Vehicle
"TacoTuneHacks", // Vehicle (HKG)
"RemapCancelToDistance", // Vehicle (GM)
"RemoteStartBootsComma", // Vehicle (GM - triggers flash)
};
// Confirmation pattern
if (StarPilotConfirmationDialog::toggleReboot(this)) {
Hardware::reboot();
}
```
#### Warning Toggles
Some toggles have warning labels:
- `NoLogging`: "Warning: ..."
- `IncreaseThermalLimits`: "Warning: ..."
- `LockDoorsTimer`: "<b>Warning:</b> openpilot can't detect if keys are still inside the car..."
#### Network Operations
Weather key testing:
```cpp
// Tests OpenWeatherMap API v2.5 and v3.0
QString url30 = "https://api.openweathermap.org/data/3.0/onecall?lat=...&appid=" + key;
QString url25 = "https://api.openweathermap.org/data/2.5/weather?lat=...&appid=" + key;
```
#### Sound Testing
Sound alert testing:
```cpp
// Offroad: Play via Python sounddevice
QString stockPath = "selfdrive/assets/sounds/" + snakeCaseAlert + ".wav";
QString themePath = "starpilot/assets/active_theme/sounds/" + snakeCaseAlert + ".wav";
float volume = params.getFloat(key) / 100.0f;
// Onroad: Send to params_memory for onroad testing
params_memory.put("TestAlert", camelCaseAlert);
```
#### Theme Name Parsing
```cpp
// Format: "name~creator" (stored lowercase)
// User-created: "name-user_created"
// Display: "Name 🌟" or "Name - by: creator"
QString baseName = value.split("~")[0];
bool userCreated = value.endsWith("-user_created");
```
#### Personality Reset Flow
Each personality has a reset button:
```cpp
// Example: Aggressive personality reset
params.putFloat("AggressiveFollow", defaultValue);
params.putFloat("AggressiveJerkAcceleration", defaultValue);
// ... other values
aggressiveFollowToggle->refresh();
aggressiveAccelerationToggle->refresh();
// ... refresh all toggles
```
#### Button State Management
Buttons can be dynamically shown/hidden/enabled:
```cpp
downloadButton->setEnabledButtons(0, !allDownloaded && online && parked);
downloadButton->setVisibleButton(1, !downloading);
downloadButton->setText(0, downloading ? "CANCEL" : "DOWNLOAD");
downloadButton->setValue("Downloading...");
```
#### Description Expansion
Qt panels track which descriptions are expanded:
```cpp
// Toggle description visibility
toggle->showDescription(); // Expand
toggle->hideDescription(); // Collapse
// Update layout after description change
connect(toggle, &AbstractControl::showDescriptionEvent, this, &Panel::update);
```
#### Model Selection Features
- Groups by series (Custom Series, Driving Policy, etc.)
- Icons: 🗺️ (Navigation), 📡 (Radar), 👀 (VOACC)
- Sort modes: alphabetical, release date, rating
- User favorites (stored in params)
- Community favorites (stored in params)
- Release dates (displayed in dialog)
---
### 9.12 Required Car State Properties
For conditional visibility, need to track:
```python
class StarPilotCarState:
# ========== Car Type Detection ==========
isGM: bool = False # General Motors
isHKG: bool = False # Hyundai/Kia/Genesis
isToyota: bool = False # Toyota/Lexus
isSubaru: bool = False # Subaru
isVolt: bool = False # 2017 Chevy Volt (specific GM)
isBolt: bool = False # Chevy Bolt (specific GM)
isAngleCar: bool = False # Uses angle-based steering
isTorqueCar: bool = False # Uses torque-based steering
isTSK: bool = False # Toyota Safety Connect
isHKGCanFd: bool = False # Hyundai/Kia with CanFD
# ========== Car Capabilities ==========
hasBSM: bool = False # Blind spot monitoring
hasRadar: bool = False # Radar sensor
hasPedal: bool = False # Pedal interceptor
hasSNG: bool = False # Stop and Go
hasNNFFLog: bool = False # NNFF log file exists
hasAutoTune: bool = False # Has auto-tune capability
hasOpenpilotLongitudinal: bool = False # Can control accel/brake
hasDashSpeedLimits: bool = False # Dashboard speed limits
hasZSS: bool = False # ZSS steering sensor
canUsePedal: bool = False # Can use pedal for longitudinal
canUseSDSU: bool = False # Can use SDSU
# ========== Device/Car State ==========
isFrogsGoMoo: bool = False # FrogsGoMoo device
hasPCMCruise: bool = False # Has PCM cruise control
lkasAllowedForAOL: bool = False # LKAS allowed for Always On Lateral
openpilotLongitudinalControlDisabled: bool = False
hasAlphaLongitudinal: bool = False
# ========== Car Values for Range Calculation ==========
# These are read from car params and used to calculate slider ranges
steerActuatorDelay: float = 0.0 # Default: car-specific
friction: float = 0.0 # Default: car-specific
steerKp: float = 0.0 # Default: car-specific
latAccelFactor: float = 0.0 # Default: car-specific
steerRatio: float = 0.0 # Default: car-specific
# Longitudinal values
longitudinalActuatorDelay: float = 0.0
startAccel: float = 0.0
stopAccel: float = 0.0
stoppingDecelRate: float = 0.0
vEgoStarting: float = 0.0
vEgoStopping: float = 0.0
```
**How Car Values Are Used**:
```python
# In showEvent(), ranges are calculated based on car values
steerKpMin = parent.steerKp * 0.5
steerKpMax = parent.steerKp * 1.5
# Title shows default value
title = f"Actuator Delay (Default: {parent.steerActuatorDelay})"
```
**How to Get Car Values**:
```cpp
// From Qt - reads from params
steerActuatorDelay = params.getFloat("SteerActuatorDelay");
if (steerActuatorDelay == 0) {
steerActuatorDelay = params.getFloat("SteerActuatorDelayDefault");
}
```
---
## 10. Technical Reference
### 10.1 Core Application Class
**File:** `system/ui/lib/application.py`
**Key Properties:**
- `width` - Screen width (2160 or 536)
- `height` - Screen height (1080 or 240)
- `target_fps` - Target frame rate (default: 60, tizi: 20)
- `frame` - Current frame number
**Key Methods:**
- `init_window(title, fps)` - Initialize raylib window
- `render()` - Main render loop as generator
- `texture(asset_path, width, height)` - Load and cache textures from openpilot assets (`selfdrive/assets/`)
- `starpilot_texture(asset_path, width, height)` - Load and cache textures from StarPilot assets (`starpilot/assets/`)
- `font(font_weight)` - Get font by weight
- `set_modal_overlay(overlay, callback)` - Show modal dialogs
**Recent Changes:**
- `_load_image_from_path` now returns early if `image.width == 0 or image.height == 0` (zero-size guard)
- `starpilot_texture()` uses `importlib.resources` to resolve `starpilot/assets/` path
### 10.2 Widget Base Class
**File:** `system/ui/widgets/__init__.py`
**Key Methods:**
- `render(rect)` - Main render entry point
- `_render(rect)` - Abstract method for subclasses
- `_process_mouse_events()` - Handle touch/click input
- `show_event()` / `hide_event()` - Lifecycle hooks
- `set_visible(visible)` - Show/hide widget
- `set_enabled(enabled)` - Enable/disable widget
### 10.3 UI State Management
**File:** `selfdrive/ui/ui_state.py`
**UIState Singleton:**
- Subscribes to message bus via `cereal.messaging.SubMaster`
- Properties: `started`, `engaged`, `status`, `is_metric`, `has_longitudinal_control`
- Methods: `update()`, `update_params()`
**Device Class:**
- Manages screen brightness, wakefulness, interactive timeouts
### 10.4 Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `BIG=1` | Force big UI on any device | 0 (false) |
| `FPS` | Target FPS | 60 (20 on tizi) |
| `SCALE` | UI scaling factor | 1.0 |
| `SHOW_FPS=1` | Display FPS counter | 0 (false) |
| `SHOW_TOUCHES=1` | Show touch points | 0 (false) |
| `ENABLE_VSYNC=1` | Enable vertical sync | 0 (false) |
### 10.5 Device Detection
**File:** `system/ui/lib/application.py`
```python
@staticmethod
def big_ui() -> bool:
return HARDWARE.get_device_type() in ('tici', 'tizi') or BIG_UI
```
**Device Types:**
- `tici` - Big UI
- `tizi` - Big UI
- `mici` - Small UI
- `pc` - Small UI (unless BIG=1)
### 10.6 New Dialog Widgets Reference
**InputDialog** (`system/ui/widgets/input_dialog.py`):
```python
InputDialog(
title: str, # Dialog title
default_text: str = "", # Pre-filled text
hint_text: str = "", # Placeholder text (gray)
on_close: Callable[[DialogResult, str], None] | None = None # (result, entered_text)
)
# Usage: text entry for Mapbox keys, weather API keys, naming themes, etc.
# Features: Keyboard widget, blinking cursor, dimmed background
```
**SelectionDialog** (`system/ui/widgets/selection_dialog.py`):
```python
SelectionDialog(
title: str,
options: list[str], # Selectable options
current_selection: int = 0, # Initially selected index
on_close: Callable[[DialogResult, int, str], None] | None = None # (result, index, text)
)
# Usage: model selection, car make/model, profile selection, theme selection
# Features: Radio-button UI (green dot), scrollable list, dimmed background
```
**ConfirmDialog** (`system/ui/widgets/confirm_dialog.py`):
```python
ConfirmDialog(
text: str,
confirm_text: str,
cancel_text: str | None = None, # None defaults to "Cancel"
rich: bool = False, # NEW: Enable HtmlRenderer for rich text
on_close: Callable[[DialogResult], None] | None = None
)
# alert_dialog(message, button_text) - convenience for single-button alerts
# Features: Rich text with HtmlRenderer + Scroller, keyboard shortcuts (Enter/Escape)
```
> [!TIP]
> **Pro Tip: Dynamic Ranges with Lambda Capture**
> For settings that depend on car-specific values (like PID/### Use the StarPilotState Singleton
Don't parse `Params` in individual widgets. Use the `starpilot_state` instance.
```python
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
# Access car state or tuning levels
if starpilot_state.car_state.isGM:
...
```
ensures that if the car state changes (e.g. from a CarParams update), the UI reflects the new limits immediately without needing a layout reset.
>
> ```python
> min_val=lambda: starpilot_state.car_state.steerKp * 0.5,
> max_val=lambda: starpilot_state.car_state.steerKp * 1.5,
> ```
### 10.7 StarPilotState Singleton Reference
**File:** `selfdrive/ui/lib/starpilot_state.py`
**Global import:** `from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state`
**Key properties:**
- `starpilot_state.car_state``StarPilotCarState` dataclass with all car type/capability/value fields
- `starpilot_state.tuning_level` — Current tuning level (0-3)
- `starpilot_state.toggle_levels` — Dict of toggle key → minimum tuning level
**Data sources parsed:**
1. `CarParamsPersistent` — Car fingerprint, lateral tuning, capabilities
2. `StarPilotCarParamsPersistent` — canUsePedal, canUseSDSU, openpilotLongitudinalControlDisabled
3. `LiveTorqueParameters` — hasAutoTune (useParams)
4. `StarPilotToggles` — JSON blob with supplementary toggles
**Update throttling:** 2.0s minimum interval between heavy param parsing
**Desktop fallback:** When `PC=True` and car is "mock", applies configurable fallback defaults (GM Bolt by default)
**Usage pattern in sub-panels:**
```python
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
# Dynamic range from car state
value_button_item(
lambda: tr("Kp Factor") + f" (Default: {starpilot_state.car_state.steerKp:.2f})",
"SteerKP",
min_val=lambda: starpilot_state.car_state.steerKp * 0.5,
max_val=lambda: starpilot_state.car_state.steerKp * 1.5,
...
)
# Conditional visibility from car state
toggle_item(
...,
enabled=lambda: not starpilot_state.car_state.isAngleCar and not starpilot_state.car_state.isTorqueCar
)
```
### 10.8 Params Helpers Reference
**File:** `common/params.py`
```python
# Reading typed params (with safe fallbacks)
params.get_int("TuningLevel", return_default=True, default=1) # → int
params.get_float("SteerDelay", return_default=True, default=0.0) # → float
# Writing typed params (auto-detects ParamKeyType)
params.put_int("IncreaseFollowingLowVisibility", 2) # Checks get_type(), casts appropriately
params.put_float("SteerDelay", 0.5) # Same type-aware behavior
# Type detection order: FLOAT → float(), INT → int(), BOOL → bool(), else → str()
```
### 10.9 Widget Factory Functions Reference
**File:** `system/ui/widgets/list_view.py`
These factory functions create standardized list items for settings panels:
```python
# Toggle item (on/off switch)
def toggle_item(
title: str | Callable[[], str],
description: str | Callable[[], str] | None = None,
initial_state: bool = False,
callback: Callable | None = None,
icon: str = "",
enabled: bool | Callable[[], bool] = True,
) -> ListItem:
# Creates a toggle switch with icon and description
action = ToggleAction(initial_state=initial_state, enabled=enabled, callback=callback)
return ListItem(title=title, description=description, action_item=action, icon=icon)
# Button item (navigation/ action)
def button_item(
title: str | Callable[[], str],
button_text: str | Callable[[], str],
description: str | Callable[[], str] | None = None,
callback: Callable | None = None,
enabled: bool | Callable[[], bool] = True,
icon: str = "",
starpilot_icon: bool = False, # For StarPilot assets
) -> ListItem:
# Creates a button with title, description, and button text
action = ButtonAction(text=button_text, enabled=enabled)
item = ListItem(title=title, description=description, action_item=action, callback=callback, icon=icon if not starpilot_icon else "")
if icon and starpilot_icon:
item.set_icon(icon, starpilot=True)
return item
# Text item (read-only display)
def text_item(
title: str | Callable[[], str],
value: str | Callable[[], str],
description: str | Callable[[], str] | None = None,
callback: Callable | None = None,
enabled: bool | Callable[[], bool] = True,
) -> ListItem:
# Displays title and value side by side
action = TextAction(text=value, color=ITEM_TEXT_VALUE_COLOR, enabled=enabled)
return ListItem(title=title, description=description, action_item=action, callback=callback)
# Dual button item (two buttons - e.g., Reboot | Power Off)
def dual_button_item(
left_text: str | Callable[[], str],
right_text: str | Callable[[], str],
left_callback: Callable = None,
right_callback: Callable = None,
description: str | Callable[[], str] | None = None,
enabled: bool | Callable[[], bool] = True,
) -> ListItem:
# Creates two buttons in one row
action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled)
return ListItem(title="", description=description, action_item=action)
# Multiple button item (selection buttons - e.g., Aggressive | Standard | Relaxed)
def multiple_button_item(
title: str | Callable[[], str],
description: str | Callable[[], str],
buttons: list[str | Callable[[], str]],
selected_index: int,
button_width: int = BUTTON_WIDTH,
callback: Callable = None,
icon: str = "",
):
# Creates multiple selectable buttons in a row
action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback)
return ListItem(title=title, description=description, icon=icon, action_item=action)
# Simple item (just title, no action)
def simple_item(title: str | Callable[[], str], callback: Callable | None = None) -> ListItem:
return ListItem(title=title, callback=callback)
```
**Value Control Factory Functions (StarPilot-specific):**
```python
# Value slider (param_key-based, reads/writes automatically)
def value_item(title, param_key, min_val, max_val, step, unit, description, callback, icon, enabled, is_metric, labels, negative)
# Value slider + action button (Reset/Test)
def value_button_item(title, param_key, min_val, max_val, step, button_text, button_callback, description, callback, icon, enabled, sub_toggles, labels, is_metric, negative)
# Two connected sliders (e.g., CESpeed "Without Lead" + "With Lead")
def dual_value_item(title, value1, value2, min_val, max_val, step, unit, label1, label2, description, callback1, callback2, icon, enabled)
# Toggle + sub-toggles (e.g., CECurves with "With Lead" sub-toggle)
def button_toggle_item(title, state, sub_toggles, sub_toggle_names, description, callback, sub_callbacks, icon, enabled, exclusive)
# Multiple action buttons (DELETE/DOWNLOAD/SELECT)
def buttons_item(title, buttons, button_callbacks, description, icon, enabled, initial_value)
# Button that opens selection dialog
def selection_button_item(title, options, selected_index, description, callback, icon, enabled)
# Read-only label display
def label_item(title, value, description, icon, enabled)
# Category buttons (horizontal row of buttons next to title)
def category_buttons_item(title, buttons, description, icon, button_width, enabled, starpilot_icon)
# Multiple selectable buttons with starpilot_icon support
def multiple_button_item(title, description, buttons, selected_index, button_width, callback, icon, starpilot_icon)
```
#### New Factor Functions Details (Added March 17, 2026)
**`dual_value_item`**:
- Parameters: `title`, `value1`, `value2`, `min_val`, `max_val`, `step`, `unit`, `label1`, `label2`, `description`, `callback1`, `callback2`, `icon`, `enabled`, `labels`, `is_metric`.
- Usage: Two connected sliders in one row (e.g. CESpeed "Without Lead" and "With Lead").
**`button_toggle_item`**:
- Parameters: `title`, `state`, `sub_toggles`, `sub_toggle_names`, `description`, `callback`, `sub_callbacks`, `icon`, `enabled`, `exclusive`.
- Usage: Main toggle with a "sub-menu" of associated buttons/toggles (e.g. CECurves with "With Lead").
**`buttons_item`**:
- Parameters: `title`, `buttons`, `button_callbacks`, `description`, `icon`, `enabled`, `initial_value`.
- Usage: Row of action buttons (e.g. themes Download/Delete/Select).
**`selection_button_item`**:
- Parameters: `title`, `options`, `selected_index`, `description`, `callback`, `icon`, `enabled`.
- Usage: Button that triggers a `SelectionDialog`.
**`label_item`**:
- Parameters: `title`, `value`, `description`, `icon`, `enabled`.
- Usage: Passive display of information (read-only).
**Key Constants:**
```python
ITEM_BASE_WIDTH = 1840 # Default item width
ITEM_BASE_HEIGHT = 126 # Default item height
ICON_SIZE = 80 # Icon dimensions
BUTTON_WIDTH = 255 # Default button width
ITEM_PADDING = 50 # Padding inside items
RIGHT_ITEM_PADDING = 20 # Spacing between items
BUTTON_FONT_SIZE = 35 # Category button text size
```
### 10.10 ListItem Class Reference
**File:** `system/ui/widgets/list_view.py`
The `ListItem` class is the base widget for all settings list items:
```python
class ListItem(Widget):
def __init__(self, title: str | Callable[[], str] = "",
icon: str | None = None,
description: str | Callable[[], str] | None = None,
description_visible: bool = False,
callback: Callable | None = None,
action_item: ItemAction | None = None):
# title: Display title (string or callable for dynamic text)
# icon: Icon filename (auto-loaded from icons/ or StarPilot assets)
# description: Expandable description text (supports HTML bold tags)
# callback: Called when action_item is activated (via set_click_callback)
# action_item: The right-side widget (Toggle, Button, Text, CategoryButtons, etc.)
```
**Key Methods:**
- `set_icon(icon, starpilot=False)` - Set icon, optionally from StarPilot assets
- `set_description(description)` - Update description text
- `set_visible(visible)` - Show/hide item
- `set_enabled(enabled)` - Enable/disable item
- `set_click_callback(callback)` - Set callback (renamed from `.callback` attribute)
- `get_item_height(font, max_width)` - Calculate item height (including expanded description)
- **Sound Test Process**: Uses a persistent Python subprocess to play sounds without blocking the UI thread or or causing `sounddevice` crashes. Spawned in `sounds.py`.
- **Panel Modularization**: The massive `starpilot.py` was successfully split into 14 distinct modules in `selfdrive/ui/layouts/settings/starpilot/` for better maintainability (March 2026).
---
> [!IMPORTANT]
> **The Golden Rule of UI Parity**
> When porting from Qt (C++), **functional parity is not enough**. You must achieve **visual parity**.
> - If the Qt UI has a 3px border, the Raylib UI must have a 3px border.
> - If the Qt UI uses a specific hex color (e.g. `#333333`), DO NOT use a generic Gray. Use the exact hex.
> - Premium interfaces feel "alive" through responsive feedback (instant label changes) and high-fidelity styling.
> - Avoid "patchwork" designs; if a widget doesn't support the required look, extend the widget rather than hacking the layout.
---
## Appendix A: File Location Reference
### Qt Files (for reference)
- `selfdrive/ui/qt/window.cc/h` - Main window
- `selfdrive/ui/qt/home.cc/h` - Home window container
- `selfdrive/ui/qt/sidebar.cc/h` - Sidebar
- `selfdrive/ui/qt/onroad/onroad_home.cc/h` - Onroad UI
- `selfdrive/ui/qt/offroad/settings.cc/h` - Settings window
### Raylib Files (current implementation)
- `common/params.py` - Extended Params wrapper (get_int, get_float, put_int, put_float)
3. **Inherit from `StarPilotPanel`**:
Sub-panels should inherit from `StarPilotPanel` and override `_render` (usually just `self._scroller.render(rect)`).
```python
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
```
---
## Appendix B: StarPilot-Specific Qt Files Reference
### Onroad
- `starpilot/ui/qt/onroad/starpilot_onroad.cc/h` - Main StarPilot onroad
- `starpilot/ui/qt/onroad/starpilot_buttons.cc/h` - Custom buttons
- `starpilot/ui/qt/onroad/starpilot_annotated_camera.cc/h` - Camera with overlays
### Offroad Settings
- `starpilot/ui/qt/offroad/starpilot_settings.cc/h` - Main StarPilot settings
- `starpilot/ui/qt/offroad/lateral_settings.cc/h` - Steering settings
- `starpilot/ui/qt/offroad/longitudinal_settings.cc/h` - Gas/brake settings
- `starpilot/ui/qt/offroad/theme_settings.cc/h` - Theme settings
- `starpilot/ui/qt/offroad/visual_settings.cc/h` - Visual settings
### Widgets
- `starpilot/ui/qt/widgets/drive_stats.cc/h` - Drive statistics
- `starpilot/ui/qt/widgets/drive_summary.cc/h` - Drive summary
- `starpilot/ui/qt/widgets/developer_sidebar.cc/h` - Developer sidebar
---
## Appendix C: Reboot Confirmation Pattern
Used by toggles that require reboot when changed while driving (ALwaysOnLateral, NNFF, ForceTorqueController, etc.):
```python
def _on_reboot_toggle(self, key, state):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
if ui_state.started:
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets import DialogResult
def _confirm_reboot(res):
gui_app.set_modal_overlay(None)
if res == DialogResult.CONFIRM:
from openpilot.system.hardware import HARDWARE
HARDWARE.reboot()
dialog = ConfirmDialog("Reboot required to take effect. Reboot now?", "Reboot", "Cancel", on_close=_confirm_reboot)
gui_app.set_modal_overlay(dialog)
```
---
## Appendix D: Sub-Panel Navigation Pattern (Lateral Example)
Pattern for implementing sub-panels within a StarPilot panel:
```python
class StarPilotLateralLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._sub_panels = {
"advanced_lateral": StarPilotAdvancedLateralLayout(),
"always_on_lateral": StarPilotAlwaysOnLateralLayout(),
# ... more sub-panels
}
# Wire up navigation for sub-panels that have their own sub-panels
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(self._navigate_to)
# Main panel items are button_items with callback=lambda: self._navigate_to("key")
items = [
button_item("Advanced Lateral Tuning", lambda: tr("MANAGE"), ...,
callback=lambda: self._navigate_to("advanced_lateral")),
]
# All underlying routing and render/show_event forwarding is handled by StarPilotPanel automatically!
```
---
*Document generated for StarPilot UI porting efforts*
*Last updated: March 17, 2026*
---
## 11. RayGUI Analysis (Aborted)
### 11.1 Investigation Hypothesis
RayGUI is an immediate-mode GUI auxiliary module natively shipped with Raylib. A hypothesis was formed that swapping the custom python-based `widgets` (`Button`, `Toggle`, `Slider`) for pyray.Gui* calls (`gui_button`, `gui_toggle`, `gui_check_box`) would simplify the code while providing parity.
### 11.2 Line-By-Line Line Findings
A systematic, line-by-line analysis of `system/ui/widgets/` was conducted. **Conclusion: RayGUI does not possess the native capability to replace the existing custom UI.**
1. **`toggle.py` (`Toggle`)**
- **Custom Implementation:** Draws a perfect pill shape natively using `draw_rectangle_rounded(..., 1.0, ...)`, a distinct white knob via `draw_circle()`, and calculates smooth interpolation between On/Off states across frame ticks. Uses Qt legacy color palettes.
- **RayGUI Limitation:** Native `gui_toggle` is a flat button. Native `gui_toggle_slider` does not natively interpolate its knob tracking smoothly; it snaps instantly. It also inherently demands text be drawn *inside* the slider pill.
2. **`button.py` (`Button`, `SmallButton`, `WideRoundedButton`)**
- **Custom Implementation:** Uses an 11-style `ButtonStyle` enum mixing disparate border thicknesses, hover alpha filters, text, and base colors dynamically. Many buttons (`SmallButton`, `WidishRoundedButton`, `IconButton`) natively map to explicit *texture assets* for standard, pressed, and disabled states (e.g. `"icons_mici/setup/small_red_pill_pressed.png"`).
- **RayGUI Limitation:** `gui_button` relies on a global style memory object. Swapping the entire structural geometry and color arrays via `GuiSetStyle()` before every button draw is messy and slower. Natively mapping 3D-shaded `.png` button textures natively to RayGUI forms is not officially supported without deep customization that defeats the simplicity of immediate-mode wrapping.
3. **`slider.py` (`SmallSlider`, `LargerSlider`)**
- **Custom Implementation:** These are highly specialized "Swipe-to-Confirm" controls incorporating `FirstOrderFilter` physics modeling for bounce-back, drag thresholds, and custom red circle draggable knobs.
- **RayGUI Limitation:** RayGUI does not offer a native swipe-to-confirm touch primitive or drag physics.
4. **`input_dialog.py` & On-Screen Interactions**
- **Custom Implementation:** Custom Touch injected keyboard.
- **RayGUI Limitation:** `gui_text_box` binds to GLFW and physical hardware events natively, breaking compatibility with our pure touch-first context.
### 11.3 Conclusion
The custom widgets authored in Python are mechanically superior, highly tailored to the automotive Qt styling aesthetics, and strictly capable of things RayGUI primitives are not (smooth physics, pure texture-mapping, swipe-to-confirm). The Raylib port will continue relying natively on the existing python `system/ui/widgets/` suite.