diff --git a/UICheatSheet.md b/UICheatSheet.md deleted file mode 100644 index 177c99e85..000000000 --- a/UICheatSheet.md +++ /dev/null @@ -1,2972 +0,0 @@ -# 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 &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 (`...`) 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("Description text here") - ``` - -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 `...` 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: "Warning: 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 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`: "Warning: 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. diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index a2cf6e6dd..727274e13 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -4,7 +4,7 @@ import math import time import pyray as rl from collections.abc import Callable -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget, DialogResult @@ -1352,3 +1352,117 @@ class TileGrid(Widget): tile = tiles_to_render[tile_idx] tile.render(rl.Rectangle(row_x + c * (row_tile_w + self._gap), rect.y + r * (tile_h + self._gap), row_tile_w, tile_h)) tile_idx += 1 + +class AetherContinuousSlider(Widget): + def __init__(self, min_val: float, max_val: float, step: float, current_val: float, on_change, title: str = "", unit: str = "", labels: dict | None = None, color: rl.Color | None = None): + super().__init__() + self.min_val = min_val + self.max_val = max_val + self.base_step = step + self.current_val = current_val + self.on_change = on_change + self.title = title + self.unit = unit + self.labels = labels or {} + self.color = color or rl.Color(54, 77, 239, 255) + + self._is_dragging = False + self._last_mouse_x = 0.0 + self._smooth_value = current_val + self._font = gui_app.font(FontWeight.BOLD) + + def _handle_mouse_press(self, mouse_pos: MousePos): + if rl.check_collision_point_rec(mouse_pos, self._rect): + self._is_dragging = True + self._last_mouse_x = mouse_pos.x + self._update_val_from_absolute(mouse_pos.x, self.base_step) + + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._is_dragging: + self._is_dragging = False + + def _handle_mouse_event(self, mouse_event: MouseEvent): + if self._is_dragging: + dt = rl.get_frame_time() + dx = mouse_event.pos.x - self._last_mouse_x + self._last_mouse_x = mouse_event.pos.x + + velocity = abs(dx / max(dt, 0.001)) + + if velocity > 1500: + step = self.base_step * 10 + elif velocity > 500: + step = self.base_step * 5 + else: + step = self.base_step + + self._update_val_from_absolute(mouse_event.pos.x, step) + + def _update_val_from_absolute(self, mouse_x: float, step: float): + track_w = self._rect.width + if track_w <= 0: return + rel_x = max(0.0, min(1.0, (mouse_x - self._rect.x) / track_w)) + val = self.min_val + rel_x * (self.max_val - self.min_val) + self._set_snapped_val(val, step) + + def _set_snapped_val(self, val: float, step: float): + snapped = round((val - self.min_val) / step) * step + self.min_val + snapped = max(self.min_val, min(self.max_val, snapped)) + if snapped != self.current_val: + self.current_val = snapped + self.on_change(self.current_val) + + def _render(self, rect: rl.Rectangle): + self.set_rect(rect) + dt = rl.get_frame_time() + + self._smooth_value += (self.current_val - self._smooth_value) * (1 - math.exp(-dt / 0.060)) + + rl.draw_rectangle_rounded(rect, 0.3, 16, rl.Color(35, 35, 40, 255)) + + frac = max(0.0, min(1.0, (self._smooth_value - self.min_val) / (self.max_val - self.min_val))) + fill_w = frac * rect.width + if fill_w > 0: + fill_rect = rl.Rectangle(rect.x, rect.y, fill_w, rect.height) + rl.draw_rectangle_rounded(fill_rect, 0.3, 16, self.color) + + if fill_w > 16: + rl.draw_rectangle_rounded(rl.Rectangle(fill_rect.x, fill_rect.y, fill_rect.width - 2, fill_rect.height - 2), 0.3, 16, rl.Color(255, 255, 255, 30)) + + title_y = rect.y + (rect.height - 24) / 2 + rl.draw_text_ex(self._font, self.title, rl.Vector2(round(rect.x + 24), round(title_y)), 24, 0, rl.WHITE) + + val_str = self.labels.get(self.current_val, f"{int(self.current_val)}{self.unit}") + ts = measure_text_cached(self._font, val_str, 24) + + text_color = rl.WHITE if frac < 0.85 else rl.Color(0, 0, 0, 180) + text_x = rect.x + rect.width - ts.x - 24 + text_y = rect.y + (rect.height - ts.y) / 2 + rl.draw_text_ex(self._font, val_str, rl.Vector2(round(text_x), round(text_y)), 24, 0, text_color) + + +def draw_toggle_pill(rect: rl.Rectangle, is_on: bool, is_enabled: bool, title: str, status_str: str, hovered: bool, pressed: bool): + if not is_enabled: + bg_color = rl.Color(35, 35, 40, 150) + elif is_on: + bg_color = AetherListColors.PRIMARY + else: + bg_color = rl.Color(35, 35, 40, 255) + + rl.draw_rectangle_rounded(rect, 0.3, 16, bg_color) + + if (hovered or pressed) and is_enabled: + overlay = rl.Color(255, 255, 255, 14 if pressed else 8) + rl.draw_rectangle_rounded(rect, 0.3, 16, overlay) + + if is_on and is_enabled: + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width - 2, rect.height - 2), 0.3, 16, rl.Color(255, 255, 255, 30)) + + font = gui_app.font(FontWeight.BOLD) + title_y = rect.y + (rect.height - 24) / 2 + text_color = rl.WHITE if is_enabled else AetherListColors.MUTED + rl.draw_text_ex(font, title, rl.Vector2(round(rect.x + 24), round(title_y)), 24, 0, text_color) + + ts = measure_text_cached(font, status_str, 24) + status_x = rect.x + rect.width - ts.x - 24 + rl.draw_text_ex(font, status_str, rl.Vector2(round(status_x), round(title_y)), 24, 0, text_color) diff --git a/selfdrive/ui/layouts/settings/starpilot/data.py b/selfdrive/ui/layouts/settings/starpilot/data.py deleted file mode 100644 index 0320b28de..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/data.py +++ /dev/null @@ -1,258 +0,0 @@ -from __future__ import annotations -import os -import shutil -import threading -import subprocess -from datetime import datetime -from pathlib import Path - -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog -from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog -from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid - -LEGACY_STARPILOT_PARAM_RENAMES = { - "FrogPilotApiToken": "StarPilotApiToken", - "FrogPilotCarParams": "StarPilotCarParams", - "FrogPilotCarParamsPersistent": "StarPilotCarParamsPersistent", - "FrogPilotDongleId": "StarPilotDongleId", - "FrogPilotStats": "StarPilotStats", -} - - -class StarPilotDataLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self._keyboard = Keyboard(min_text_size=0) - self.CATEGORIES = [ - {"title": tr_noop("Manage Backups"), "panel": "backups", "icon": "toggle_icons/icon_system.png", "color": "#D43D8A"}, - {"title": tr_noop("Toggle Backups"), "panel": "toggle_backups", "icon": "toggle_icons/icon_system.png", "color": "#D43D8A"}, - {"title": tr_noop("Driving Data Storage"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "icon": "toggle_icons/icon_system.png", "color": "#D43D8A", "is_enabled": lambda: False}, - { - "title": tr_noop("Delete Driving Data"), - "type": "hub", - "on_click": self._on_delete_driving_data, - "icon": "toggle_icons/icon_system.png", - "color": "#D43D8A", - }, - { - "title": tr_noop("Delete Error Logs"), - "type": "hub", - "on_click": self._on_delete_error_logs, - "icon": "toggle_icons/icon_system.png", - "color": "#D43D8A", - }, - ] - - self._sub_panels = { - "backups": StarPilotBackupsLayout(), - "toggle_backups": StarPilotToggleBackupsLayout(), - } - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - - for name, panel in self._sub_panels.items(): - if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(self._navigate_to) - if hasattr(panel, 'set_back_callback'): - panel.set_back_callback(self._go_back) - - self._rebuild_grid() - - def _on_delete_driving_data(self): - def _do_delete(res): - if res == DialogResult.CONFIRM: - - def _task(): - drive_paths = ["/data/media/0/realdata/", "/data/media/0/realdata_HD/", "/data/media/0/realdata_konik/"] - for path in drive_paths: - p = Path(path) - if p.exists(): - for entry in p.iterdir(): - if entry.is_dir(): - shutil.rmtree(entry, ignore_errors=True) - - threading.Thread(target=_task, daemon=True).start() - gui_app.set_modal_overlay(alert_dialog(tr("Driving data deletion started."))) - - gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all driving data and footage?"), tr("Delete"), on_close=_do_delete)) - - def _on_delete_error_logs(self): - def _do_delete(res): - if res == DialogResult.CONFIRM: - shutil.rmtree("/data/error_logs", ignore_errors=True) - os.makedirs("/data/error_logs", exist_ok=True) - gui_app.set_modal_overlay(alert_dialog(tr("Error logs deleted."))) - - gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all error logs?"), tr("Delete"), on_close=_do_delete)) - - def _get_storage(self): - paths = ["/data/media/0/osm/offline", "/data/media/0/realdata", "/data/backups"] - total = 0 - for p in paths: - pp = Path(p) - if pp.exists(): - total += sum(f.stat().st_size for f in pp.rglob('*') if f.is_file()) - mb = total / (1024 * 1024) - if mb > 1024: - return f"{(mb / 1024):.2f} GB" - return f"{mb:.2f} MB" - - -class StarPilotBackupsLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - self.CATEGORIES = [ - {"title": tr_noop("Create Backup"), "type": "hub", "on_click": self._on_create_backup, "color": "#D43D8A"}, - {"title": tr_noop("Restore Backup"), "type": "hub", "on_click": self._on_restore_backup, "color": "#D43D8A"}, - {"title": tr_noop("Delete Backup"), "type": "hub", "on_click": self._on_delete_backup, "color": "#D43D8A"}, - ] - self._rebuild_grid() - - def _get_backups(self): - b_dir = Path("/data/backups") - if not b_dir.exists(): - return [] - return [f.name for f in b_dir.glob("*.tar.zst") if "in_progress" not in f.name] - - def _on_create_backup(self): - def on_name(res, name): - if res == DialogResult.CONFIRM: - safe_name = name.replace(" ", "_") if name else f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - backup_path = f"/data/backups/{safe_name}.tar.zst" - if Path(backup_path).exists(): - gui_app.set_modal_overlay(alert_dialog(tr("A backup with this name already exists."))) - return - gui_app.set_modal_overlay(alert_dialog(tr("Backup creation started."))) - - def _task(): - os.makedirs("/data/backups", exist_ok=True) - subprocess.run(["tar", "--use-compress-program=zstd", "-cf", backup_path, "/data/openpilot"]) - - threading.Thread(target=_task, daemon=True).start() - - self._keyboard.reset(min_text_size=0) - self._keyboard.set_title(tr("Name your backup"), "") - self._keyboard.set_text("") - self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text)) - gui_app.push_widget(self._keyboard) - - def _on_restore_backup(self): - backups = self._get_backups() - if not backups: - gui_app.set_modal_overlay(alert_dialog(tr("No backups found."))) - return - dialog = MultiOptionDialog(tr("Select Backup"), backups) - - def _on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - gui_app.set_modal_overlay(alert_dialog(tr("Restoring... device will reboot."))) - - def _task(): - subprocess.run(["rm", "-rf", "/data/openpilot/*"]) - subprocess.run(["tar", "--use-compress-program=zstd", "-xf", f"/data/backups/{dialog.selection}", "-C", "/"]) - os.system("reboot") - - threading.Thread(target=_task, daemon=True).start() - - gui_app.set_modal_overlay(dialog, callback=_on_select) - - def _on_delete_backup(self): - backups = self._get_backups() - if not backups: - gui_app.set_modal_overlay(alert_dialog(tr("No backups found."))) - return - dialog = MultiOptionDialog(tr("Delete Backup"), backups) - - def _on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - os.remove(f"/data/backups/{dialog.selection}") - self._rebuild_grid() - - gui_app.set_modal_overlay(dialog, callback=_on_select) - - -class StarPilotToggleBackupsLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self._keyboard = Keyboard(min_text_size=0) - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - self.CATEGORIES = [ - {"title": tr_noop("Create Toggle Backup"), "type": "hub", "on_click": self._on_create, "color": "#D43D8A"}, - {"title": tr_noop("Restore Toggle Backup"), "type": "hub", "on_click": self._on_restore, "color": "#D43D8A"}, - {"title": tr_noop("Delete Toggle Backup"), "type": "hub", "on_click": self._on_delete, "color": "#D43D8A"}, - ] - self._rebuild_grid() - - def _get_backups(self): - b_dir = Path("/data/toggle_backups") - if not b_dir.exists(): - return [] - return [d.name for d in b_dir.iterdir() if d.is_dir() and "in_progress" not in d.name] - - def _on_create(self): - def on_name(res, name): - if res == DialogResult.CONFIRM: - safe_name = name.replace(" ", "_") if name else f"toggle_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - backup_path = Path(f"/data/toggle_backups/{safe_name}") - if backup_path.exists(): - gui_app.set_modal_overlay(alert_dialog(tr("A toggle backup with this name already exists."))) - return - os.makedirs(backup_path, exist_ok=True) - shutil.copytree("/data/params/d", str(backup_path), dirs_exist_ok=True) - gui_app.set_modal_overlay(alert_dialog(tr("Toggle backup created."))) - self._rebuild_grid() - - self._keyboard.reset(min_text_size=0) - self._keyboard.set_title(tr("Name your toggle backup"), "") - self._keyboard.set_text("") - self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text)) - gui_app.push_widget(self._keyboard) - - def _on_restore(self): - backups = self._get_backups() - if not backups: - gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found."))) - return - dialog = MultiOptionDialog(tr("Select Toggle Backup"), backups) - - def _on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - - def on_confirm(r2): - if r2 == DialogResult.CONFIRM: - src = Path(f"/data/toggle_backups/{dialog.selection}") - params_dir = Path("/data/params/d") - for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items(): - if (src / old_key).exists(): - (params_dir / new_key).unlink(missing_ok=True) - shutil.copytree(str(src), "/data/params/d", dirs_exist_ok=True) - for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items(): - old_path = params_dir / old_key - new_path = params_dir / new_key - if old_path.exists(): - old_path.replace(new_path) - gui_app.set_modal_overlay(alert_dialog(tr("Toggles restored."))) - self._rebuild_grid() - - gui_app.set_modal_overlay(ConfirmDialog(tr("This will overwrite your current toggles."), tr("Restore"), on_close=on_confirm)) - - gui_app.set_modal_overlay(dialog, callback=_on_select) - - def _on_delete(self): - backups = self._get_backups() - if not backups: - gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found."))) - return - dialog = MultiOptionDialog(tr("Delete Toggle Backup"), backups) - - def _on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - shutil.rmtree(f"/data/toggle_backups/{dialog.selection}", ignore_errors=True) - self._rebuild_grid() - - gui_app.set_modal_overlay(dialog, callback=_on_select) diff --git a/selfdrive/ui/layouts/settings/starpilot/device.py b/selfdrive/ui/layouts/settings/starpilot/device.py deleted file mode 100644 index c20b9f575..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/device.py +++ /dev/null @@ -1,292 +0,0 @@ -from __future__ import annotations -from pathlib import Path - -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog -from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog, TileGrid - - -class StarPilotDeviceLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - {"title": tr_noop("Screen Settings"), "panel": "screen", "icon": "toggle_icons/icon_light.png", "color": "#D43D8A"}, - {"title": tr_noop("Device Settings"), "panel": "device_settings", "icon": "toggle_icons/icon_device.png", "color": "#D43D8A"}, - { - "title": tr_noop("Device Shutdown"), - "type": "value", - "get_value": self._get_shutdown_timer, - "on_click": self._show_shutdown_selector, - "color": "#D43D8A", - }, - { - "title": tr_noop("Disable Logging"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("NoLogging"), - "set_state": lambda s: self._params.put_bool("NoLogging", s), - "color": "#D43D8A", - }, - { - "title": tr_noop("Disable Uploads"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("NoUploads"), - "set_state": lambda s: self._params.put_bool("NoUploads", s), - "color": "#D43D8A", - }, - { - "title": tr_noop("Disable Onroad Uploads"), - "type": "toggle", - "param": "DisableOnroadUploads", - "get_state": lambda: self._params.get_bool("DisableOnroadUploads"), - "set_state": lambda s: self._params.put_bool("DisableOnroadUploads", s), - "color": "#D43D8A", - }, - { - "title": tr_noop("High-Quality Recording"), - "type": "toggle", - "param": "HigherBitrate", - "get_state": lambda: self._params.get_bool("HigherBitrate"), - "set_state": lambda s: self._on_higher_bitrate_toggle(s), - "color": "#D43D8A", - }, - ] - - self._sub_panels = { - "screen": StarPilotScreenLayout(), - "device_settings": StarPilotDeviceManagementLayout(), - } - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - - for name, panel in self._sub_panels.items(): - if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(self._navigate_to) - if hasattr(panel, 'set_back_callback'): - panel.set_back_callback(self._go_back) - - self._rebuild_grid() - - def _rebuild_grid(self): - no_uploads = self._params.get_bool("NoUploads") - disable_onroad = self._params.get_bool("DisableOnroadUploads") - filtered = [] - for cat in self.CATEGORIES: - param = cat.get("param") - if param == "DisableOnroadUploads" and not no_uploads: - continue - if param == "HigherBitrate" and (not no_uploads or disable_onroad): - continue - filtered.append(cat) - original = self.CATEGORIES - self.CATEGORIES = filtered - super()._rebuild_grid() - self.CATEGORIES = original - - def _on_higher_bitrate_toggle(self, state): - self._params.put_bool("HigherBitrate", state) - cache_path = Path("/cache/use_HD") - if state: - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.touch() - else: - if cache_path.exists(): - cache_path.unlink() - if ui_state.started: - gui_app.set_modal_overlay( - ConfirmDialog( - tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None - ) - ) - - def _get_shutdown_timer(self): - v = self._params.get_int("DeviceShutdown") - if v == 0: - return tr("5 mins") - if v <= 3: - return f"{v * 15} mins" - return f"{v - 3} " + (tr("hour") if v == 4 else tr("hours")) - - def _show_shutdown_selector(self): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int("DeviceShutdown", int(val)) - self._rebuild_grid() - - labels = {0: tr("5 mins")} - for i in range(1, 4): - labels[i] = f"{i * 15} mins" - for i in range(4, 34): - labels[i] = f"{i - 3} " + (tr("hour") if i == 4 else tr("hours")) - - gui_app.set_modal_overlay(AetherSliderDialog(tr("Device Shutdown"), 0, 33, 1, self._params.get_int("DeviceShutdown"), on_close, labels=labels, color="#D43D8A")) - - -class StarPilotScreenLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - self.CATEGORIES = [ - { - "title": tr_noop("Screen Settings"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("ScreenManagement"), - "set_state": lambda s: self._params.put_bool("ScreenManagement", s), - "icon": "toggle_icons/icon_light.png", - "color": "#D43D8A", - }, - { - "title": tr_noop("Brightness (Offroad)"), - "type": "value", - "get_value": lambda: self._get_brightness("ScreenBrightness"), - "on_click": lambda: self._show_brightness_selector("ScreenBrightness"), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("ScreenManagement"), - }, - { - "title": tr_noop("Brightness (Onroad)"), - "type": "value", - "get_value": lambda: self._get_brightness("ScreenBrightnessOnroad"), - "on_click": lambda: self._show_brightness_selector("ScreenBrightnessOnroad"), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("ScreenManagement"), - }, - { - "title": tr_noop("Timeout (Offroad)"), - "type": "value", - "get_value": lambda: f"{self._params.get_int('ScreenTimeout')}s", - "on_click": lambda: self._show_timeout_selector("ScreenTimeout"), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("ScreenManagement"), - }, - { - "title": tr_noop("Timeout (Onroad)"), - "type": "value", - "get_value": lambda: f"{self._params.get_int('ScreenTimeoutOnroad')}s", - "on_click": lambda: self._show_timeout_selector("ScreenTimeoutOnroad"), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("ScreenManagement"), - }, - { - "title": tr_noop("Standby Mode"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("StandbyMode"), - "set_state": lambda s: self._params.put_bool("StandbyMode", s), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("ScreenManagement"), - }, - ] - self._rebuild_grid() - - def _get_brightness(self, key): - v = self._params.get_int(key) - if key == "ScreenBrightnessOnroad" and v == 0: - v = 1 - if v == 0: - return tr("Off") - if v == 101: - return tr("Auto") - return f"{v}%" - - def _show_brightness_selector(self, key): - def on_close(res, val): - if res == DialogResult.CONFIRM: - new_v = int(val) - if key == "ScreenBrightnessOnroad": - new_v = max(new_v, 1) - self._params.put_int(key, new_v) - HARDWARE.set_brightness(new_v) - self._rebuild_grid() - - min_value = 1 if key == "ScreenBrightnessOnroad" else 0 - current_value = max(self._params.get_int(key), min_value) - labels = {101: tr("Auto")} - if min_value == 0: - labels[0] = tr("Off") - - gui_app.set_modal_overlay( - AetherSliderDialog(tr(key), min_value, 101, 1, current_value, on_close, unit="%", labels=labels, color="#D43D8A") - ) - - def _show_timeout_selector(self, key): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(key, int(val)) - self._rebuild_grid() - - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 5, 60, 5, self._params.get_int(key), on_close, unit="s", color="#D43D8A")) - - -class StarPilotDeviceManagementLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - self.CATEGORIES = [ - { - "title": tr_noop("Device Settings"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("DeviceManagement"), - "set_state": lambda s: self._params.put_bool("DeviceManagement", s), - "icon": "toggle_icons/icon_device.png", - "color": "#D43D8A", - }, - { - "title": tr_noop("Low-Voltage Cutoff"), - "type": "value", - "get_value": lambda: f"{self._params.get_float('LowVoltageShutdown'):.1f}V", - "on_click": self._show_voltage_selector, - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("DeviceManagement"), - }, - { - "title": tr_noop("Raise Temp Limits"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("IncreaseThermalLimits"), - "set_state": lambda s: self._params.put_bool("IncreaseThermalLimits", s), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("DeviceManagement"), - }, - { - "title": tr_noop("Use Konik Server"), - "type": "toggle", - "get_state": lambda: self._get_konik_state(), - "set_state": lambda s: self._on_konik_toggle(s), - "color": "#D43D8A", - "visible": lambda: self._params.get_bool("DeviceManagement"), - }, - ] - self._rebuild_grid() - - def _get_konik_state(self): - if Path("/data/not_vetted").exists(): - return True - return self._params.get_bool("UseKonikServer") - - def _on_konik_toggle(self, state): - self._params.put_bool("UseKonikServer", state) - cache_path = Path("/cache/use_konik") - if state: - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.touch() - else: - if cache_path.exists(): - cache_path.unlink() - if ui_state.started: - gui_app.set_modal_overlay( - ConfirmDialog( - tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None - ) - ) - - def _show_voltage_selector(self): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_float("LowVoltageShutdown", float(val)) - self._rebuild_grid() - - gui_app.set_modal_overlay( - AetherSliderDialog(tr("Low-Voltage Cutoff"), 11.8, 12.5, 0.1, self._params.get_float("LowVoltageShutdown"), on_close, unit="V", color="#D43D8A") - ) diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index 654b62c23..0cfc61b80 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -105,14 +105,14 @@ class StarPilotAdvancedLateralLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_float(key, float(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) 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: dialog = ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None) - gui_app.set_modal_overlay(dialog) + gui_app.push_widget(dialog) class StarPilotAlwaysOnLateralLayout(StarPilotPanel): def __init__(self): @@ -129,13 +129,13 @@ class StarPilotAlwaysOnLateralLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) 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: - gui_app.set_modal_overlay(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)) + gui_app.push_widget(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)) class StarPilotLaneChangesLayout(StarPilotPanel): def __init__(self): @@ -155,14 +155,14 @@ class StarPilotLaneChangesLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) def _show_float_selector(self, key, min_v, max_v, step, unit=""): def on_close(res, val): if res == DialogResult.CONFIRM: self._params.put_float(key, float(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) class StarPilotLateralTuneLayout(StarPilotPanel): def __init__(self): @@ -192,7 +192,7 @@ class StarPilotLateralTuneLayout(StarPilotPanel): self._params.put_bool(key, state) from openpilot.selfdrive.ui.ui_state import ui_state if ui_state.started: - gui_app.set_modal_overlay(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)) + gui_app.push_widget(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)) class StarPilotLateralQOLLayout(StarPilotPanel): def __init__(self): @@ -208,7 +208,7 @@ class StarPilotLateralQOLLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) class StarPilotLateralLayout(StarPilotPanel): def __init__(self): diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py index 35edd6e35..45fb18c1b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py +++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py @@ -240,7 +240,7 @@ class StarPilotAdvancedLongitudinalLayout(StarPilotPanel): self._params.put_float(key, float(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) class StarPilotConditionalExperimentalLayout(StarPilotPanel): @@ -372,7 +372,7 @@ class StarPilotConditionalExperimentalLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) def _show_int_selector(self, key, min_v, max_v, unit=""): def on_close(res, val): @@ -380,7 +380,7 @@ class StarPilotConditionalExperimentalLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) class StarPilotCurveSpeedLayout(StarPilotPanel): @@ -441,7 +441,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel): self._params.remove("CurvatureData") self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr("Reset Curve Data?"), tr("Confirm"), on_close=on_close)) + gui_app.push_widget(ConfirmDialog(tr("Reset Curve Data?"), tr("Confirm"), on_close=on_close)) class StarPilotPersonalitiesLayout(StarPilotPanel): @@ -537,7 +537,7 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel): self._params.remove(self._profile + key) self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr("Reset to Defaults?"), tr("Confirm"), on_close=on_close)) + gui_app.push_widget(ConfirmDialog(tr("Reset to Defaults?"), tr("Confirm"), on_close=on_close)) def _show_float_selector(self, key, min_v, max_v, step, unit=""): def on_close(res, val): @@ -545,7 +545,7 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel): self._params.put_float(key, float(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) def _show_int_selector(self, key, min_v, max_v, unit=""): def on_close(res, val): @@ -553,7 +553,7 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 5, self._params.get_int(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 5, self._params.get_int(key), on_close, unit=unit, color="#597497")) class StarPilotLongitudinalTuneLayout(StarPilotPanel): @@ -669,7 +669,7 @@ class StarPilotLongitudinalTuneLayout(StarPilotPanel): self._params.put_int(key, label_to_value[dialog.selection]) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) def _show_int_selector(self, key, min_v, max_v, unit=""): def on_close(res, val): @@ -677,7 +677,7 @@ class StarPilotLongitudinalTuneLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) class StarPilotLongitudinalQOLLayout(StarPilotPanel): @@ -782,7 +782,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): self._rebuild_grid() current = max(1, self._params.get_int(key)) - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 1, 100, 1, current, on_close, unit=" mph", color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), 1, 100, 1, current, on_close, unit=" mph", color="#597497")) def _show_int_selector(self, key, min_v, max_v, unit=""): def on_close(res, val): @@ -790,7 +790,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) class StarPilotSpeedLimitControllerLayout(StarPilotPanel): @@ -867,7 +867,7 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel): secondary_options = ["None"] + [option for option in ("Dashboard", "Map Data", "Vision") if option != primary] selected_secondary = current_secondary if current_secondary in secondary_options else "None" secondary_dialog = MultiOptionDialog(tr("SLC Secondary Priority"), secondary_options, selected_secondary) - gui_app.set_modal_overlay(secondary_dialog, callback=lambda res: on_secondary_select(primary, secondary_dialog, res)) + gui_app.push_widget(secondary_dialog, callback=lambda res: on_secondary_select(primary, secondary_dialog, res)) primary_dialog = MultiOptionDialog(tr("SLC Primary Priority"), primary_options, current_primary) @@ -881,7 +881,7 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel): return show_secondary_dialog(primary_dialog.selection) - gui_app.set_modal_overlay(primary_dialog, callback=on_primary_select) + gui_app.push_widget(primary_dialog, callback=on_primary_select) def _show_selection(self, key, options): current = self._params.get(key, encoding='utf-8') or "None" @@ -892,7 +892,7 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel): self._params.put(key, dialog.selection) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) class StarPilotSLCOffsetsLayout(StarPilotPanel): @@ -928,7 +928,7 @@ class StarPilotSLCOffsetsLayout(StarPilotPanel): self._rebuild_grid() min_value, max_value = self._speed_range() - gui_app.set_modal_overlay( + gui_app.push_widget( AetherSliderDialog(tr(key), min_value, max_value, 1, self._params.get_int(key), on_close, unit=self._speed_unit(), color="#597497") ) @@ -997,7 +997,7 @@ class StarPilotSLCQOLLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497")) class StarPilotSLCVisualsLayout(StarPilotPanel): @@ -1065,9 +1065,9 @@ class StarPilotWeatherLayout(StarPilotPanel): self._params.remove("WeatherAPIKey") self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr("Remove API Key?"), tr("Confirm"), on_close=on_confirm)) + gui_app.push_widget(ConfirmDialog(tr("Remove API Key?"), tr("Confirm"), on_close=on_confirm)) - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) class StarPilotWeatherBase(StarPilotPanel): @@ -1117,4 +1117,4 @@ class StarPilotWeatherBase(StarPilotPanel): self._rebuild_grid() curr = self._params.get_int(key) - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, curr, on_close, unit=unit, color="#597497")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, curr, on_close, unit=unit, color="#597497")) diff --git a/selfdrive/ui/layouts/settings/starpilot/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py index 0af004dbf..de1e25d14 100644 --- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py @@ -14,10 +14,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.longitudinal import StarP from openpilot.selfdrive.ui.layouts.settings.starpilot.lateral import StarPilotLateralLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.maps import StarPilotMapsLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.navigation import StarPilotNavigationLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.data import StarPilotDataLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.device import StarPilotDeviceLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.system_settings import StarPilotSystemLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.utilities import StarPilotUtilitiesLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.visuals import StarPilotVisualsLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotThemesLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout @@ -94,9 +91,6 @@ class StarPilotLayout(Widget): StarPilotPanelType.LATERAL: StarPilotPanelInfo(tr_noop("Steering"), StarPilotLateralLayout()), StarPilotPanelType.MAPS: StarPilotPanelInfo(tr_noop("Map Data"), StarPilotMapsLayout()), StarPilotPanelType.NAVIGATION: StarPilotPanelInfo(tr_noop("Navigation"), StarPilotNavigationLayout()), - StarPilotPanelType.DATA: StarPilotPanelInfo(tr_noop("Data Management"), StarPilotDataLayout()), - StarPilotPanelType.DEVICE: StarPilotPanelInfo(tr_noop("Device Controls"), StarPilotDeviceLayout()), - StarPilotPanelType.UTILITIES: StarPilotPanelInfo(tr_noop("Utilities"), StarPilotUtilitiesLayout()), StarPilotPanelType.VISUALS: StarPilotPanelInfo(tr_noop("Appearance"), StarPilotVisualsLayout()), StarPilotPanelType.THEMES: StarPilotPanelInfo(tr_noop("Themes"), StarPilotThemesLayout()), StarPilotPanelType.VEHICLE: StarPilotPanelInfo(tr_noop("Vehicle Settings"), StarPilotVehicleSettingsLayout()), @@ -201,9 +195,6 @@ class StarPilotLayout(Widget): "LATERAL": StarPilotPanelType.LATERAL, "MAPS": StarPilotPanelType.MAPS, "NAVIGATION": StarPilotPanelType.NAVIGATION, - "DATA": StarPilotPanelType.DATA, - "DEVICE": StarPilotPanelType.DEVICE, - "UTILITIES": StarPilotPanelType.UTILITIES, "VISUALS": StarPilotPanelType.VISUALS, "THEMES": StarPilotPanelType.THEMES, "VEHICLE": StarPilotPanelType.VEHICLE, diff --git a/selfdrive/ui/layouts/settings/starpilot/maps.py b/selfdrive/ui/layouts/settings/starpilot/maps.py index 3513b4fc6..8a61a3ed3 100644 --- a/selfdrive/ui/layouts/settings/starpilot/maps.py +++ b/selfdrive/ui/layouts/settings/starpilot/maps.py @@ -124,7 +124,7 @@ class StarPilotMapsLayout(StarPilotPanel): self._params.put("PreferredSchedule", schedule_param_value(dialog.selection)) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) def _on_download(self): current_selected = self._params.get("MapsSelected", encoding="utf-8") or "" @@ -133,15 +133,15 @@ class StarPilotMapsLayout(StarPilotPanel): self._params.put("MapsSelected", selected_raw) selected = [k.strip() for k in selected_raw.split(",") if k.strip()] if not selected: - gui_app.set_modal_overlay(alert_dialog(tr("Please select at least one region or state first!"))) + gui_app.push_widget(alert_dialog(tr("Please select at least one region or state first!"))) return def on_confirm(res): if res == DialogResult.CONFIRM: self._params_memory.put_bool("DownloadMaps", True) - gui_app.set_modal_overlay(alert_dialog(tr("Map download started in background."))) + gui_app.push_widget(alert_dialog(tr("Map download started in background."))) - gui_app.set_modal_overlay(ConfirmDialog(tr("Start downloading maps for selected regions?"), tr("Download"), on_close=on_confirm)) + gui_app.push_widget(ConfirmDialog(tr("Start downloading maps for selected regions?"), tr("Download"), on_close=on_confirm)) def _on_remove(self): def on_confirm(res): @@ -149,7 +149,7 @@ class StarPilotMapsLayout(StarPilotPanel): maps_path = Path("/data/media/0/osm/offline") if maps_path.exists(): shutil.rmtree(maps_path, ignore_errors=True) - gui_app.set_modal_overlay(alert_dialog(tr("Maps removed."))) + gui_app.push_widget(alert_dialog(tr("Maps removed."))) self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all downloaded map data?"), tr("Remove"), on_close=on_confirm)) + gui_app.push_widget(ConfirmDialog(tr("Delete all downloaded map data?"), tr("Remove"), on_close=on_confirm)) diff --git a/selfdrive/ui/layouts/settings/starpilot/navigation.py b/selfdrive/ui/layouts/settings/starpilot/navigation.py index 47f9ff921..4dd2c748b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/navigation.py +++ b/selfdrive/ui/layouts/settings/starpilot/navigation.py @@ -51,7 +51,7 @@ class StarPilotNavigationLayout(StarPilotPanel): self._rebuild_grid() def _on_setup(self): - gui_app.set_modal_overlay( + gui_app.push_widget( alert_dialog(tr("Mapbox Setup:\n1. Create account at mapbox.com\n2. Generate Public/Secret keys\n3. Add keys in 'Mapbox Credentials'")) ) @@ -134,7 +134,7 @@ class StarPilotMapboxLayout(StarPilotPanel): self._params.remove(key) self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr(f"Remove your {key.replace('Mapbox', '')} key?"), tr("Remove"), on_close=on_remove)) + gui_app.push_widget(ConfirmDialog(tr(f"Remove your {key.replace('Mapbox', '')} key?"), tr("Remove"), on_close=on_remove)) else: def on_close(res, text): diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index 8d91143ee..8f3054d1b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -1,4 +1,5 @@ from __future__ import annotations +import math import subprocess import time from pathlib import Path @@ -7,14 +8,213 @@ import pyray as rl from openpilot.common.basedir import BASEDIR from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import gui_label from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.tabbed_panel import TabSectionSpec, TabbedSectionHost -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, SPACING +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_LIST_METRICS, + AetherListColors, + build_list_panel_frame, + draw_list_panel_shell, + AetherContinuousSlider, + draw_toggle_pill, +) + +MODEL_PANEL_BG = AetherListColors.PANEL_BG +MODEL_HEADER_TEXT = AetherListColors.HEADER +MODEL_SUBTEXT = AetherListColors.SUBTEXT +MODEL_MUTED = AetherListColors.MUTED + +SECTION_GAP = AETHER_LIST_METRICS.section_gap + + + +class SoundsManagerView(Widget): + def __init__(self, controller: "StarPilotSoundsLayout"): + super().__init__() + self._controller = controller + self._pressed_target: str | None = None + + self._sliders: dict[str, AetherContinuousSlider] = {} + self._slider_was_dragging: dict[str, bool] = {} + self._toggle_rects: dict[str, rl.Rectangle] = {} + self._font = gui_app.font(FontWeight.BOLD) + + self._init_sliders() + + def _init_sliders(self): + for key in self._controller.VOLUME_KEYS: + val = self._controller._params.get_int(key, return_default=True, default=100) + + def on_change(v, k=key): + new_v = int(v) + if new_v != 101 and new_v < self._controller.VOLUME_INFO[k]["min"]: + new_v = self._controller.VOLUME_INFO[k]["min"] + self._controller._params.put_int(k, new_v) + + slider = AetherContinuousSlider( + min_val=0.0, + max_val=101.0, + step=1.0, + current_val=float(val), + on_change=on_change, + title=tr(self._controller.VOLUME_INFO[key]["title"]), + unit="%", + labels={0.0: tr("Muted"), 101.0: tr("Auto")}, + color=AetherListColors.PRIMARY + ) + self._sliders[key] = slider + self._slider_was_dragging[key] = False + + cd_val = self._controller._params.get_int(self._controller.COOLDOWN_KEY, return_default=True, default=0) + def on_cd_change(v): + self._controller._params.put_int(self._controller.COOLDOWN_KEY, int(v)) + + cd_slider = AetherContinuousSlider( + min_val=0.0, + max_val=float(self._controller.COOLDOWN_INFO["max"]), + step=1.0, + current_val=float(cd_val), + on_change=on_cd_change, + title=tr(self._controller.COOLDOWN_INFO["title"]), + unit=" " + tr("min"), + labels={0.0: tr("Off"), 1.0: tr("1 minute")}, + color=AetherListColors.PRIMARY + ) + self._sliders[self._controller.COOLDOWN_KEY] = cd_slider + self._slider_was_dragging[self._controller.COOLDOWN_KEY] = False + + def _handle_mouse_press(self, mouse_pos: MousePos): + self._pressed_target = self._target_at(mouse_pos) + for slider in self._sliders.values(): + slider._handle_mouse_press(mouse_pos) + + def _handle_mouse_release(self, mouse_pos: MousePos): + for slider in self._sliders.values(): + slider._handle_mouse_release(mouse_pos) + + target = self._target_at(mouse_pos) + if self._pressed_target is not None and self._pressed_target == target: + self._activate_target(target) + self._pressed_target = None + + def _handle_mouse_event(self, mouse_event: MouseEvent): + for slider in self._sliders.values(): + slider._handle_mouse_event(mouse_event) + + def _target_at(self, mouse_pos: MousePos) -> str | None: + for key, rect in self._toggle_rects.items(): + if rl.check_collision_point_rec(mouse_pos, rect): + return f"toggle:{key}" + return None + + def _activate_target(self, target: str): + if target.startswith("toggle:"): + key = target.split(":", 1)[1] + info = self._controller.ALERT_INFO.get(key) + if info and info.get("is_enabled", lambda: True)(): + current = self._controller._params.get_bool(key) + self._controller._params.put_bool(key, not current) + + def _render(self, rect: rl.Rectangle): + self.set_rect(rect) + self._toggle_rects.clear() + + frame = build_list_panel_frame(rect) + draw_list_panel_shell(frame) + + header_rect = frame.header + self._draw_header(header_rect) + + # Reclaim the dead space! The global header allocates 210px, but our text only uses ~100px. + metrics = AETHER_LIST_METRICS + actual_header_height = 100 + content_y = header_rect.y + actual_header_height + content_h = (frame.shell.y + frame.shell.height) - content_y - metrics.panel_padding_bottom + + content_rect = rl.Rectangle( + frame.scroll.x, + content_y, + frame.scroll.width, + content_h + ) + + col_width = (content_rect.width - SECTION_GAP) / 2 + left_col = rl.Rectangle(content_rect.x, content_rect.y, col_width, content_rect.height) + right_col = rl.Rectangle(content_rect.x + col_width + SECTION_GAP, content_rect.y, col_width, content_rect.height) + + self._draw_volume_section(left_col) + self._draw_utility_section(right_col) + + for key, slider in self._sliders.items(): + is_dragging = slider._is_dragging + if self._slider_was_dragging[key] and not is_dragging: + if key in self._controller.VOLUME_KEYS: + self._controller._test_sound(key) + self._slider_was_dragging[key] = is_dragging + + def _draw_header(self, rect: rl.Rectangle): + title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40) + gui_label(title_rect, tr("Sounds & Alerts"), 40, MODEL_HEADER_TEXT, FontWeight.SEMI_BOLD) + + subtitle_rect = rl.Rectangle(rect.x, rect.y + 48, rect.width * 0.58, 36) + gui_label(subtitle_rect, tr("Manage system volumes and custom alert toggles."), 24, MODEL_SUBTEXT, FontWeight.NORMAL) + + def _draw_volume_section(self, rect: rl.Rectangle): + num_volumes = len(self._controller.VOLUME_KEYS) + vol_row_h = rect.height / num_volumes + + for index, key in enumerate(self._controller.VOLUME_KEYS): + row_rect = rl.Rectangle(rect.x, rect.y + index * vol_row_h, rect.width, vol_row_h) + self._draw_slider_row(row_rect, key, self._controller.VOLUME_INFO[key]) + + def _draw_utility_section(self, rect: rl.Rectangle): + total_elements = 7 # 1 cooldown + 6 alerts + row_h = rect.height / total_elements + + # Cooldown Slider (Index 0) + cd_row_rect = rl.Rectangle(rect.x, rect.y, rect.width, row_h) + self._draw_slider_row(cd_row_rect, self._controller.COOLDOWN_KEY, self._controller.COOLDOWN_INFO) + + # Custom Alert Toggle Pills (Indices 1 to 6) + for index, key in enumerate(self._controller.CUSTOM_ALERTS_KEYS): + row_rect = rl.Rectangle(rect.x, rect.y + (index + 1) * row_h, rect.width, row_h) + self._draw_toggle_row(row_rect, key, self._controller.ALERT_INFO[key]) + + def _draw_slider_row(self, rect: rl.Rectangle, key: str, info: dict): + slider = self._sliders[key] + + padded_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width - 12, rect.height - 8) + + if not slider._is_dragging: + current_param = self._controller._params.get_int(key, return_default=True, default=100 if key != self._controller.COOLDOWN_KEY else 0) + if slider.current_val != current_param: + slider.current_val = float(current_param) + + slider.render(padded_rect) + + def _draw_toggle_row(self, rect: rl.Rectangle, key: str, info: dict): + padded_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width - 12, rect.height - 8) + + current_val = self._controller._params.get_bool(key) + is_enabled = info.get("is_enabled", lambda: True)() + + mouse_pos = gui_app.last_mouse_event.pos + hovered = rl.check_collision_point_rec(mouse_pos, padded_rect) + pressed = self._pressed_target == f"toggle:{key}" + + status_str = tr("ON") if current_val else tr("OFF") + if not is_enabled: status_str = tr(info.get("disabled_label", "UNAVAILABLE")) + + draw_toggle_pill(padded_rect, current_val, is_enabled, tr(info["title"]), status_str, hovered, pressed) + + self._toggle_rects[key] = padded_rect + class StarPilotSoundsLayout(StarPilotPanel): COOLDOWN_KEY = "SwitchbackModeCooldown" @@ -37,42 +237,16 @@ class StarPilotSoundsLayout(StarPilotPanel): "SpeedLimitChangedAlert", ] - def __init__(self): - super().__init__() - self._section_tabs = TabbedSectionHost([ - TabSectionSpec("volume_control", "Volumes", StarPilotVolumeControlLayout()), - TabSectionSpec("custom_alerts", "Alerts", StarPilotCustomAlertsLayout()), - ]) - - def set_navigate_callback(self, callback): - self._section_tabs.set_navigate_callback(callback) - - def set_back_callback(self, callback): - self._section_tabs.set_back_callback(callback) - - def _render(self, rect): - self._section_tabs.render(rect) - - def set_current_sub_panel(self, sub_panel: str): - self._section_tabs.set_current_sub_panel(sub_panel) - - def show_event(self): - self._section_tabs.show_event() - - def hide_event(self): - self._section_tabs.hide_event() - -class StarPilotVolumeControlLayout(StarPilotPanel): - COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "icon": "toggle_icons/icon_mute.png", "min": 0, "max": 30} + COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "min": 0, "max": 30} VOLUME_INFO = { - "BelowSteerSpeedVolume": {"title": tr_noop("Min Steer Speed Alert"), "icon": "toggle_icons/icon_mute.png", "min": 0}, - "DisengageVolume": {"title": tr_noop("Disengage Volume"), "icon": "toggle_icons/icon_mute.png", "min": 0}, - "EngageVolume": {"title": tr_noop("Engage Volume"), "icon": "toggle_icons/icon_green_light.png", "min": 0}, - "PromptVolume": {"title": tr_noop("Prompt Volume"), "icon": "toggle_icons/icon_message.png", "min": 0}, - "PromptDistractedVolume": {"title": tr_noop("Distracted Volume"), "icon": "toggle_icons/icon_display.png", "min": 0}, - "RefuseVolume": {"title": tr_noop("Refuse Volume"), "icon": "toggle_icons/icon_mute.png", "min": 0}, - "WarningSoftVolume": {"title": tr_noop("Warning Soft"), "icon": "toggle_icons/icon_conditional.png", "min": 25}, - "WarningImmediateVolume": {"title": tr_noop("Warning Immediate"), "icon": "toggle_icons/icon_conditional.png", "min": 25}, + "BelowSteerSpeedVolume": {"title": tr_noop("Min Steer Speed Alert"), "min": 0}, + "DisengageVolume": {"title": tr_noop("Disengage Volume"), "min": 0}, + "EngageVolume": {"title": tr_noop("Engage Volume"), "min": 0}, + "PromptVolume": {"title": tr_noop("Prompt Volume"), "min": 0}, + "PromptDistractedVolume": {"title": tr_noop("Distracted Volume"), "min": 0}, + "RefuseVolume": {"title": tr_noop("Refuse Volume"), "min": 0}, + "WarningSoftVolume": {"title": tr_noop("Warning Soft"), "min": 25}, + "WarningImmediateVolume": {"title": tr_noop("Warning Immediate"), "min": 25}, } _sound_player_process = None @@ -81,71 +255,35 @@ class StarPilotVolumeControlLayout(StarPilotPanel): super().__init__() self._init_sound_player() - self.SECTIONS = [ - { - "title": tr_noop("Volume Levels"), - "categories": self._build_volume_categories(), + self.ALERT_INFO = { + "GoatScream": {"title": tr_noop("Goat Scream")}, + "GoatScreamCriticalAlerts": {"title": tr_noop("Goat Critical")}, + "GreenLightAlert": {"title": tr_noop("Green Light")}, + "LeadDepartingAlert": {"title": tr_noop("Lead Departure")}, + "LoudBlindspotAlert": { + "title": tr_noop("Loud Blindspot"), + "is_enabled": lambda: starpilot_state.car_state.hasBSM, + "disabled_label": tr_noop("Needs BSM") }, - { - "title": tr_noop("Safety & Cooldown"), - "categories": self._build_safety_categories(), - } - ] - self._rebuild_grid() + "SpeedLimitChangedAlert": { + "title": tr_noop("Speed Limit"), + "is_enabled": lambda: self._params.get_bool("ShowSpeedLimits") or ( + starpilot_state.car_state.hasOpenpilotLongitudinal and self._params.get_bool("SpeedLimitController") + ), + "disabled_label": tr_noop("Needs Speed Limits") + }, + } - def _build_volume_categories(self): - cats = [] - for key in StarPilotSoundsLayout.VOLUME_KEYS: - info = self.VOLUME_INFO[key] + self._manager_view = SoundsManagerView(self) - def get_val(k=key): - return float(self._params.get_int(k, return_default=True, default=100)) + def _render(self, rect: rl.Rectangle): + self._manager_view.render(rect) - def set_val(val, k=key): - new_v = int(val) - if new_v != 101 and new_v < self.VOLUME_INFO[k]["min"]: - new_v = self.VOLUME_INFO[k]["min"] - self._params.put_int(k, new_v) + def show_event(self): + super().show_event() - def test_cb(k=key): - self._test_sound(k) - - cats.append({ - "title": info["title"], - "type": "slider", - "get_value": get_val, - "set_value": set_val, - "on_test": test_cb, - "min_val": 0, - "max_val": 101, - "step": 1, - "unit": "%", - "labels": {0: tr("Muted"), 101: tr("Auto")}, - "icon": info["icon"], - "color": "#3B82F6", - }) - return cats - - def _build_safety_categories(self): - def get_cooldown_val(): - return float(self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0)) - - def set_cooldown_val(val): - self._params.put_int(StarPilotSoundsLayout.COOLDOWN_KEY, int(val)) - - return [{ - "title": self.COOLDOWN_INFO["title"], - "type": "slider", - "get_value": get_cooldown_val, - "set_value": set_cooldown_val, - "min_val": 0, - "max_val": float(self.COOLDOWN_INFO["max"]), - "step": 1, - "unit": " " + tr("min"), - "labels": {0: tr("Off"), 1: tr("1 minute")}, - "icon": self.COOLDOWN_INFO["icon"], - "color": "#3B82F6", - }] + def hide_event(self): + super().hide_event() @classmethod def _init_sound_player(cls): @@ -194,60 +332,3 @@ while True: self._sound_player_process.stdin.write(f"{sound_path}|{volume}\n".encode()) self._sound_player_process.stdin.flush() except: pass - -class StarPilotCustomAlertsLayout(StarPilotPanel): - ALERT_INFO = { - "GoatScream": {"title": tr_noop("Goat Scream"), "icon": "toggle_icons/icon_sound.png"}, - "GoatScreamCriticalAlerts": {"title": tr_noop("Goat Critical"), "icon": "toggle_icons/icon_sound.png"}, - "GreenLightAlert": {"title": tr_noop("Green Light"), "icon": "toggle_icons/icon_green_light.png"}, - "LeadDepartingAlert": {"title": tr_noop("Lead Departure"), "icon": "toggle_icons/icon_steering.png"}, - "LoudBlindspotAlert": {"title": tr_noop("Loud Blindspot"), "icon": "toggle_icons/icon_display.png"}, - "SpeedLimitChangedAlert": {"title": tr_noop("Speed Limit"), "icon": "toggle_icons/icon_speed_limit.png"}, - } - - def __init__(self): - super().__init__() - self._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True) - self.CATEGORIES = [] - for key in StarPilotSoundsLayout.CUSTOM_ALERTS_KEYS: - info = self.ALERT_INFO[key] - self.CATEGORIES.append({ - "title": info["title"], - "type": "toggle", - "get_state": lambda k=key: self._params.get_bool(k), - "set_state": lambda s, k=key: self._params.put_bool(k, s), - "icon": info["icon"], - "color": "#3B82F6", - "key": key # Store for visibility check - }) - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - self._tile_grid.clear() - - for cat in self.CATEGORIES: - key = cat.get("key") - is_enabled = lambda: True - disabled_label = "" - - if key == "LoudBlindspotAlert": - is_enabled = lambda: starpilot_state.car_state.hasBSM - disabled_label = tr_noop("Needs BSM") - elif key == "SpeedLimitChangedAlert": - is_enabled = lambda: self._params.get_bool("ShowSpeedLimits") or ( - starpilot_state.car_state.hasOpenpilotLongitudinal and self._params.get_bool("SpeedLimitController") - ) - disabled_label = tr_noop("Needs Speed Limits") - - tile = ToggleTile( - title=tr(cat["title"]), - get_state=cat["get_state"], - set_state=cat["set_state"], - icon_path=cat.get("icon"), - bg_color=cat.get("color"), - is_enabled=is_enabled, - disabled_label=tr(disabled_label) if disabled_label else "", - ) - self._tile_grid.add_tile(tile) diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index c38f5eccc..22a5b3fa1 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -1,92 +1,700 @@ from __future__ import annotations +import json +import os +import shutil +import subprocess +import threading +import time +from datetime import datetime +from pathlib import Path +from dataclasses import replace import pyray as rl +from openpilot.system.hardware import HARDWARE +from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 +from openpilot.system.ui.widgets import DialogResult, Widget +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog +from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets.label import gui_label +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import RadioTileGroup -from openpilot.selfdrive.ui.layouts.settings.starpilot.data import StarPilotDataLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.device import StarPilotDeviceLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.utilities import StarPilotUtilitiesLayout +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_LIST_METRICS, + AetherListColors, + AetherScrollbar, + AetherContinuousSlider, + build_list_panel_frame, + draw_list_panel_shell, + draw_list_scroll_fades, + draw_toggle_pill, +) + +LEGACY_STARPILOT_PARAM_RENAMES = { + "FrogPilotApiToken": "StarPilotApiToken", + "FrogPilotCarParams": "StarPilotCarParams", + "FrogPilotCarParamsPersistent": "StarPilotCarParamsPersistent", + "FrogPilotDongleId": "StarPilotDongleId", + "FrogPilotStats": "StarPilotStats", +} + +EXCLUDED_KEYS = { + "AvailableModels", + "AvailableModelNames", + "StarPilotStats", + "GithubSshKeys", + "GithubUsername", + "MapBoxRequests", + "ModelDrivesAndScores", + "OverpassRequests", + "SpeedLimits", + "SpeedLimitsFiltered", + "UpdaterAvailableBranches", +} + +REPORT_CATEGORIES = [ + "Acceleration feels harsh or jerky", + "An alert was unclear and I'm not sure what it meant", + "Braking is too sudden or uncomfortable", + "I'm not sure if this is normal or a bug:", + "My steering wheel buttons aren't working", + "openpilot disengages when I don't expect it", + "openpilot feels sluggish or slow to respond", + "Something else (please describe)", +] + +class SystemSettingsManagerView(Widget): + def __init__(self, controller: "StarPilotSystemLayout"): + super().__init__() + self._controller = controller + self._scroll_panel = GuiScrollPanel2(horizontal=False) + self._scrollbar = AetherScrollbar() + self._content_height = 0.0 + self._scroll_offset = 0.0 + self._pressed_target: str | None = None + self._can_click = True + + self._action_rects: dict[str, rl.Rectangle] = {} + self._toggle_rects: dict[str, rl.Rectangle] = {} + self._shell_rect = rl.Rectangle(0, 0, 0, 0) + self._scroll_rect = rl.Rectangle(0, 0, 0, 0) + + shutdown_labels = {0: tr("5 mins")} + for i in range(1, 4): shutdown_labels[i] = f"{i * 15} mins" + for i in range(4, 34): shutdown_labels[i] = f"{i - 3} " + (tr("hour") if i == 4 else tr("hours")) + + brightness_labels = {101: tr("Auto"), 0: tr("Off")} + + self._sliders = { + "ScreenBrightness": AetherContinuousSlider(0, 101, 1, self._controller._params.get_int("ScreenBrightness"), lambda v: self._controller._set_brightness("ScreenBrightness", v), title=tr("Brightness (Offroad)"), unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY), + "ScreenBrightnessOnroad": AetherContinuousSlider(1, 101, 1, max(1, self._controller._params.get_int("ScreenBrightnessOnroad")), lambda v: self._controller._set_brightness("ScreenBrightnessOnroad", max(1, int(v))), title=tr("Brightness (Onroad)"), unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY), + "ScreenTimeout": AetherContinuousSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeout"), lambda v: self._controller._params.put_int("ScreenTimeout", int(v)), title=tr("Timeout (Offroad)"), unit="s", color=AetherListColors.PRIMARY), + "ScreenTimeoutOnroad": AetherContinuousSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeoutOnroad"), lambda v: self._controller._params.put_int("ScreenTimeoutOnroad", int(v)), title=tr("Timeout (Onroad)"), unit="s", color=AetherListColors.PRIMARY), + "DeviceShutdown": AetherContinuousSlider(0, 33, 1, self._controller._params.get_int("DeviceShutdown"), lambda v: self._controller._params.put_int("DeviceShutdown", int(v)), title=tr("Device Shutdown"), labels=shutdown_labels, color=AetherListColors.PRIMARY), + "LowVoltageShutdown": AetherContinuousSlider(11.8, 12.5, 0.1, self._controller._params.get_float("LowVoltageShutdown"), lambda v: self._controller._params.put_float("LowVoltageShutdown", float(v)), title=tr("Low-Voltage Cutoff"), unit="V", color=AetherListColors.PRIMARY), + } + + def _clear_ephemeral_state(self): + self._pressed_target = None + self._can_click = True + + def show_event(self): + super().show_event() + self._clear_ephemeral_state() + + def hide_event(self): + super().hide_event() + self._clear_ephemeral_state() + + def _handle_mouse_press(self, mouse_pos: MousePos): + self._pressed_target = None + self._can_click = True + + for action_id, rect in self._action_rects.items(): + visible_rect = rl.get_collision_rec(rect, self._scroll_rect) + if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect): + self._pressed_target = f"action:{action_id}" + return + + for toggle_id, rect in self._toggle_rects.items(): + visible_rect = rl.get_collision_rec(rect, self._scroll_rect) + if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect): + self._pressed_target = f"toggle:{toggle_id}" + return + + for slider in self._sliders.values(): + slider._handle_mouse_press(mouse_pos) + + def _handle_mouse_event(self, mouse_event: MouseEvent): + if not self._scroll_panel.is_touch_valid(): + self._can_click = False + for slider in self._sliders.values(): + slider._handle_mouse_event(mouse_event) + + def _handle_mouse_release(self, mouse_pos: MousePos): + target = self._pressed_target + self._pressed_target = None + + if target and self._can_click: + if target.startswith("action:"): + action_id = target.split(":", 1)[1] + rect = self._action_rects.get(action_id) + elif target.startswith("toggle:"): + toggle_id = target.split(":", 1)[1] + rect = self._toggle_rects.get(toggle_id) + + if rect: + visible_rect = rl.get_collision_rec(rect, self._scroll_rect) + if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect): + self._activate_target(target) + + for slider in self._sliders.values(): + slider._handle_mouse_release(mouse_pos) + + def _activate_target(self, target: str): + action_id = target.split(":", 1)[1] + self._controller.handle_action(action_id) + + def _render(self, rect: rl.Rectangle): + self.set_rect(rect) + self._action_rects.clear() + self._toggle_rects.clear() + + metrics = replace(AETHER_LIST_METRICS, header_height=110) + frame = build_list_panel_frame(rect, metrics) + self._shell_rect = frame.shell + draw_list_panel_shell(frame) + + header_rect = frame.header + self._draw_header(header_rect) + + scroll_rect = frame.scroll + self._scroll_rect = scroll_rect + + content_width = scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter + self._content_height = self._measure_content_height() + self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height)) + + rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height)) + self._draw_scroll_content(scroll_rect, content_width) + rl.end_scissor_mode() + + if self._content_height > scroll_rect.height: + self._draw_scrollbar(scroll_rect) + + draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=AETHER_LIST_METRICS.fade_height) + + def _draw_header(self, rect: rl.Rectangle): + title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40) + gui_label(title_rect, tr("System Settings"), 40, AetherListColors.HEADER, FontWeight.SEMI_BOLD) + subtitle_rect = rl.Rectangle(rect.x, rect.y + 48, rect.width * 0.58, 36) + gui_label(subtitle_rect, tr("Manage device behavior, power, and storage seamlessly."), 24, AetherListColors.SUBTEXT, FontWeight.NORMAL) + + def _measure_column_height(self, sections: list[dict]) -> float: + total_height = 0 + for section in sections: + total_height += AETHER_LIST_METRICS.section_header_height + AETHER_LIST_METRICS.section_header_gap + for row in section["rows"]: + if row["type"] == "slider": + total_height += 100 + 16 + elif row["type"] in ["toggle", "toggle_row"]: + total_height += 90 + 16 + elif row["type"] == "action_group": + total_height += 110 + 16 + total_height += AETHER_LIST_METRICS.section_gap + return max(total_height - AETHER_LIST_METRICS.section_gap, 0.0) + + def _measure_content_height(self) -> float: + cols = self._controller.utility_columns() + return max(self._measure_column_height(cols["left"]), self._measure_column_height(cols["right"]), 0.0) + + def _draw_scroll_content(self, rect: rl.Rectangle, width: float): + cols = self._controller.utility_columns() + col_w = (width - AETHER_LIST_METRICS.section_gap) / 2 + left_x = rect.x + right_x = rect.x + col_w + AETHER_LIST_METRICS.section_gap + + self._draw_column(rl.Rectangle(left_x, rect.y + self._scroll_offset, col_w, rect.height), cols["left"]) + self._draw_column(rl.Rectangle(right_x, rect.y + self._scroll_offset, col_w, rect.height), cols["right"]) + + def _draw_column(self, rect: rl.Rectangle, sections: list[dict]): + y = rect.y + mouse_pos = gui_app.last_mouse_event.pos + + for section in sections: + title_rect = rl.Rectangle(rect.x, y, rect.width, AETHER_LIST_METRICS.section_header_height) + gui_label(title_rect, section["title"], 26, AetherListColors.SUBTEXT, FontWeight.MEDIUM) + y += AETHER_LIST_METRICS.section_header_height + AETHER_LIST_METRICS.section_header_gap + + for row in section["rows"]: + if row["type"] == "slider": + slider = self._sliders[row["id"]] + slider.render(rl.Rectangle(rect.x, y, rect.width, 100)) + y += 100 + 16 + elif row["type"] == "toggle_row": + items = row["items"] + item_w = (rect.width - 16 * (len(items) - 1)) / len(items) + for i, item in enumerate(items): + enabled = item.get("enabled", True) + toggle_rect = rl.Rectangle(rect.x + i * (item_w + 16), y, item_w, 90) + if enabled: + self._toggle_rects[item["id"]] = toggle_rect + hovered = rl.check_collision_point_rec(mouse_pos, toggle_rect) + pressed = self._pressed_target == f"toggle:{item['id']}" + draw_toggle_pill(toggle_rect, item["value"], enabled, item["title"], tr("ON") if item["value"] else tr("OFF"), hovered, pressed) + y += 90 + 16 + elif row["type"] == "toggle": + enabled = row.get("enabled", True) + toggle_rect = rl.Rectangle(rect.x, y, rect.width, 90) + if enabled: + self._toggle_rects[row["id"]] = toggle_rect + hovered = rl.check_collision_point_rec(mouse_pos, toggle_rect) + pressed = self._pressed_target == f"toggle:{row['id']}" + draw_toggle_pill(toggle_rect, row["value"], enabled, row["title"], tr("ON") if row["value"] else tr("OFF"), hovered, pressed) + y += 90 + 16 + elif row["type"] == "action_group": + group_rect = rl.Rectangle(rect.x, y, rect.width, 110) + self._draw_action_group(group_rect, row, mouse_pos) + y += 110 + 16 + + y += AETHER_LIST_METRICS.section_gap + + def _draw_action_group(self, rect: rl.Rectangle, row: dict, mouse_pos: MousePos): + rl.draw_rectangle_rounded(rect, 0.3, 16, rl.Color(35, 35, 40, 255)) + + title_y = rect.y + (rect.height - 24) / 2 + gui_label(rl.Rectangle(rect.x + 24, title_y, rect.width * 0.4, 24), row["title"], 24, rl.WHITE, FontWeight.BOLD) + + actions = row["actions"] + btn_gap = 12 + available_w = rect.width * 0.6 - 40 + btn_w = (available_w - (len(actions) - 1) * btn_gap) / len(actions) + start_x = rect.x + rect.width - available_w - 16 + + for i, action in enumerate(actions): + btn_rect = rl.Rectangle(start_x + i * (btn_w + btn_gap), rect.y + 12, btn_w, rect.height - 24) + self._action_rects[action["id"]] = btn_rect + + hovered = rl.check_collision_point_rec(mouse_pos, btn_rect) + pressed = self._pressed_target == f"action:{action['id']}" + + active = action.get("active", False) + color = AetherListColors.PRIMARY if active else rl.Color(60, 60, 65, 255) + if action.get("danger"): + color = AetherListColors.DANGER + + if hovered: color = rl.Color(min(color.r + 20, 255), min(color.g + 20, 255), min(color.b + 20, 255), 255) + if pressed: color = rl.Color(max(color.r - 20, 0), max(color.g - 20, 0), max(color.b - 20, 0), 255) + + rl.draw_rectangle_rounded(btn_rect, 0.4, 16, color) + gui_label(btn_rect, action["label"], 24, rl.WHITE, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + + def _draw_scrollbar(self, rect: rl.Rectangle): + self._scrollbar.render(rect, self._content_height, self._scroll_offset) class StarPilotSystemLayout(StarPilotPanel): def __init__(self): super().__init__() - - self._section_names = ["device", "data_and_backups", "utilities"] - self._active_section = self._section_names[0] - self._sub_panels = { - "device": StarPilotDeviceLayout(), - "data_and_backups": StarPilotDataLayout(), - "utilities": StarPilotUtilitiesLayout(), - } - - self._section_tabs = RadioTileGroup( - "", - [tr("Device"), tr("Data"), tr("Utilities")], - 0, - self._on_section_change, - ) - - for name, panel in self._sub_panels.items(): - if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(lambda sub_panel, section_name=name: self._navigate_to_child(section_name, sub_panel)) - if hasattr(panel, 'set_back_callback'): - panel.set_back_callback(self._go_back) - - def _on_section_change(self, index: int): - if 0 <= index < len(self._section_names): - previous_panel = self._sub_panels[self._active_section] - if hasattr(previous_panel, 'set_current_sub_panel'): - previous_panel.set_current_sub_panel("") - self._current_sub_panel = "" - self._set_active_section(self._section_names[index], "") - if self._navigate_callback: - self._navigate_callback("") - - def _set_active_section(self, section_name: str, child_panel: str = ""): - if section_name not in self._sub_panels: - return - - if section_name != self._active_section: - self._sub_panels[self._active_section].hide_event() - self._active_section = section_name - self._sub_panels[self._active_section].show_event() - - self._section_tabs.set_index(self._section_names.index(section_name)) - panel = self._sub_panels[section_name] - if hasattr(panel, 'set_current_sub_panel'): - panel.set_current_sub_panel(child_panel) - - def _navigate_to_child(self, section_name: str, child_panel: str): - self._set_active_section(section_name, child_panel) - if self._navigate_callback: - self._navigate_callback(f"{section_name}:{child_panel}") - - def set_current_sub_panel(self, sub_panel: str): - super().set_current_sub_panel(sub_panel) - if not sub_panel: - self._set_active_section(self._active_section, "") - return - - if ":" in sub_panel: - section_name, child_panel = sub_panel.split(":", 1) - self._set_active_section(section_name, child_panel) - elif sub_panel in self._section_names: - self._set_active_section(sub_panel) - - def _render(self, rect): - tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, 110) - panel_rect = rl.Rectangle(rect.x, rect.y + 140, rect.width, rect.height - 140) - self._section_tabs.render(tab_rect) - self._sub_panels[self._active_section].render(panel_rect) + self._keyboard = Keyboard(min_text_size=0) + self._manager_view = SystemSettingsManagerView(self) def show_event(self): super().show_event() - self._sub_panels[self._active_section].show_event() + self._manager_view.show_event() def hide_event(self): super().hide_event() - self._sub_panels[self._active_section].hide_event() + self._manager_view.hide_event() + + def _render(self, rect: rl.Rectangle): + self._manager_view.render(rect) + + def utility_columns(self) -> dict[str, list[dict]]: + state = self._get_force_drive_state() + no_uploads = self._params.get_bool("NoUploads") + disable_onroad = self._params.get_bool("DisableOnroadUploads") + + screen_management = self._params.get_bool("ScreenManagement") + screen_rows = [ + {"id": "ScreenManagement", "type": "toggle", "title": tr("Screen Management"), "value": screen_management}, + {"id": "StandbyMode", "type": "toggle", "title": tr("Standby Mode"), "value": self._params.get_bool("StandbyMode"), "enabled": screen_management}, + ] + if screen_management: + screen_rows.extend([ + {"id": "ScreenBrightness", "type": "slider"}, + {"id": "ScreenBrightnessOnroad", "type": "slider"}, + {"id": "ScreenTimeout", "type": "slider"}, + {"id": "ScreenTimeoutOnroad", "type": "slider"}, + ]) + + device_management = self._params.get_bool("DeviceManagement") + device_rows = [ + {"id": "DeviceManagement", "type": "toggle", "title": tr("Device Management"), "value": device_management}, + {"id": "IncreaseThermalLimits", "type": "toggle", "title": tr("Raise Thermal Limits"), "value": self._params.get_bool("IncreaseThermalLimits"), "enabled": device_management}, + ] + if device_management: + device_rows.extend([ + {"id": "DeviceShutdown", "type": "slider"}, + {"id": "LowVoltageShutdown", "type": "slider"}, + ]) + + network_rows = [ + {"type": "toggle_row", "items": [ + {"id": "NoUploads", "title": tr("Disable Uploads"), "value": no_uploads}, + {"id": "UseKonikServer", "title": tr("Use Konik Server"), "value": self._get_konik_state()} + ]}, + {"type": "toggle_row", "items": [ + {"id": "DisableOnroadUploads", "title": tr("Disable Onroad Uploads"), "value": disable_onroad, "enabled": not no_uploads}, + {"id": "NoLogging", "title": tr("Disable Logging"), "value": self._params.get_bool("NoLogging")} + ]}, + {"id": "HigherBitrate", "type": "toggle", "title": tr("High-Quality Recording"), "value": self._params.get_bool("HigherBitrate"), "enabled": not disable_onroad and not no_uploads} + ] + + data_rows = [ + {"id": "Storage", "type": "action_group", "title": tr("Storage & Logs"), "actions": [ + {"id": "Storage", "label": f"{tr('Clear Data')} ({self._get_storage()})", "danger": True}, + {"id": "ErrorLogs", "label": tr("Clear Logs"), "danger": True} + ]}, + {"id": "SystemBackups", "type": "action_group", "title": tr("System Backups"), "actions": [ + {"id": "CreateBackup", "label": tr("Create")}, + {"id": "RestoreBackup", "label": tr("Restore")}, + {"id": "DeleteBackup", "label": tr("Delete"), "danger": True} + ]}, + {"id": "ToggleBackups", "type": "action_group", "title": tr("Toggle Backups"), "actions": [ + {"id": "CreateToggleBackup", "label": tr("Create")}, + {"id": "RestoreToggleBackup", "label": tr("Restore")}, + {"id": "DeleteToggleBackup", "label": tr("Delete"), "danger": True} + ]}, + ] + + util_rows = [ + {"type": "toggle_row", "items": [{"id": "DebugMode", "type": "toggle", "title": tr("Debug Mode"), "value": self._params.get_bool("DebugMode")}]}, + {"id": "ForceDriveState", "type": "action_group", "title": tr("Force Drive State"), "actions": [ + {"id": "DriveDefault", "label": tr("Auto"), "active": state == tr("Default")}, + {"id": "DriveOnroad", "label": tr("Onroad"), "active": state == tr("Onroad")}, + {"id": "DriveOffroad", "label": tr("Offroad"), "active": state == tr("Offroad")} + ]}, + {"id": "QuickActions", "type": "action_group", "title": tr("Quick Actions"), "actions": [ + {"id": "FlashPanda", "label": tr("Flash Panda")}, + {"id": "ReportIssue", "label": tr("Report Issue")} + ]}, + {"id": "FactoryReset", "type": "action_group", "title": tr("Factory Reset"), "actions": [ + {"id": "ResetDefaults", "label": tr("Toggles"), "danger": True}, + {"id": "ResetStock", "label": tr("Stock OP"), "danger": True} + ]}, + ] + + return { + "left": [ + {"title": tr("Display Configuration"), "rows": screen_rows}, + {"title": tr("Developer & Maintenance"), "rows": util_rows}, + ], + "right": [ + {"title": tr("Power & Thermals"), "rows": device_rows}, + {"title": tr("Networking & Data"), "rows": network_rows}, + {"title": tr("Data & Backups"), "rows": data_rows}, + ] + } + + def handle_action(self, action_id: str): + if action_id == "ScreenManagement": + self._params.put_bool("ScreenManagement", not self._params.get_bool("ScreenManagement")) + elif action_id == "StandbyMode": + self._params.put_bool("StandbyMode", not self._params.get_bool("StandbyMode")) + elif action_id == "DeviceManagement": + self._params.put_bool("DeviceManagement", not self._params.get_bool("DeviceManagement")) + elif action_id == "IncreaseThermalLimits": + self._params.put_bool("IncreaseThermalLimits", not self._params.get_bool("IncreaseThermalLimits")) + elif action_id == "UseKonikServer": + self._on_konik_toggle(not self._get_konik_state()) + elif action_id == "NoLogging": + self._params.put_bool("NoLogging", not self._params.get_bool("NoLogging")) + elif action_id == "NoUploads": + self._params.put_bool("NoUploads", not self._params.get_bool("NoUploads")) + elif action_id == "DisableOnroadUploads": + self._params.put_bool("DisableOnroadUploads", not self._params.get_bool("DisableOnroadUploads")) + elif action_id == "HigherBitrate": + self._on_higher_bitrate_toggle(not self._params.get_bool("HigherBitrate")) + elif action_id == "Storage": + self._on_delete_driving_data() + elif action_id == "ErrorLogs": + self._on_delete_error_logs() + elif action_id == "CreateBackup": + self._on_create_backup() + elif action_id == "RestoreBackup": + self._on_restore_backup() + elif action_id == "DeleteBackup": + self._on_delete_backup() + elif action_id == "CreateToggleBackup": + self._on_create_toggle_backup() + elif action_id == "RestoreToggleBackup": + self._on_restore_toggle_backup() + elif action_id == "DeleteToggleBackup": + self._on_delete_toggle_backup() + elif action_id == "DebugMode": + self._params.put_bool("DebugMode", not self._params.get_bool("DebugMode")) + elif action_id == "DriveDefault": + self._params.put_bool("ForceOffroad", False) + self._params.put_bool("ForceOnroad", False) + elif action_id == "DriveOnroad": + self._params.put_bool("ForceOnroad", True) + self._params.put_bool("ForceOffroad", False) + elif action_id == "DriveOffroad": + self._params.put_bool("ForceOffroad", True) + self._params.put_bool("ForceOnroad", False) + elif action_id == "FlashPanda": + self._on_flash_panda() + elif action_id == "ReportIssue": + self._on_report_issue() + elif action_id == "ResetDefaults": + self._on_reset_defaults() + elif action_id == "ResetStock": + self._on_reset_stock() + + def _set_brightness(self, key, val): + self._params.put_int(key, int(val)) + if key == "ScreenBrightnessOnroad" or key == "ScreenBrightness": + HARDWARE.set_brightness(int(val)) + + def _get_konik_state(self): + if Path("/data/not_vetted").exists(): + return True + return self._params.get_bool("UseKonikServer") + + def _on_konik_toggle(self, state): + self._params.put_bool("UseKonikServer", state) + cache_path = Path("/cache/use_konik") + if state: + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.touch() + else: + if cache_path.exists(): + cache_path.unlink() + if ui_state.started: + gui_app.push_widget( + ConfirmDialog( + tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None + ) + ) + + def _on_higher_bitrate_toggle(self, state): + self._params.put_bool("HigherBitrate", state) + cache_path = Path("/cache/use_HD") + if state: + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.touch() + else: + if cache_path.exists(): + cache_path.unlink() + if ui_state.started: + gui_app.push_widget( + ConfirmDialog( + tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None + ) + ) + + def _get_storage(self): + paths = ["/data/media/0/osm/offline", "/data/media/0/realdata", "/data/backups"] + total = 0 + for p in paths: + pp = Path(p) + if pp.exists(): + total += sum(f.stat().st_size for f in pp.rglob('*') if f.is_file()) + mb = total / (1024 * 1024) + if mb > 1024: + return f"{(mb / 1024):.2f} GB" + return f"{mb:.2f} MB" + + def _on_delete_driving_data(self): + def _do_delete(res): + if res == DialogResult.CONFIRM: + def _task(): + drive_paths = ["/data/media/0/realdata/", "/data/media/0/realdata_HD/", "/data/media/0/realdata_konik/"] + for path in drive_paths: + p = Path(path) + if p.exists(): + for entry in p.iterdir(): + if entry.is_dir(): + shutil.rmtree(entry, ignore_errors=True) + threading.Thread(target=_task, daemon=True).start() + gui_app.push_widget(alert_dialog(tr("Driving data deletion started."))) + gui_app.push_widget(ConfirmDialog(tr("Delete all driving data and footage?"), tr("Delete"), on_close=_do_delete)) + + def _on_delete_error_logs(self): + def _do_delete(res): + if res == DialogResult.CONFIRM: + shutil.rmtree("/data/error_logs", ignore_errors=True) + os.makedirs("/data/error_logs", exist_ok=True) + gui_app.push_widget(alert_dialog(tr("Error logs deleted."))) + gui_app.push_widget(ConfirmDialog(tr("Delete all error logs?"), tr("Delete"), on_close=_do_delete)) + + def _get_backups(self, folder="backups"): + b_dir = Path(f"/data/{folder}") + if not b_dir.exists(): + return [] + if folder == "backups": + return [f.name for f in b_dir.glob("*.tar.zst") if "in_progress" not in f.name] + return [d.name for d in b_dir.iterdir() if d.is_dir() and "in_progress" not in d.name] + + def _on_create_backup(self): + def on_name(res, name): + if res == DialogResult.CONFIRM: + safe_name = name.replace(" ", "_") if name else f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + backup_path = f"/data/backups/{safe_name}.tar.zst" + if Path(backup_path).exists(): + gui_app.push_widget(alert_dialog(tr("A backup with this name already exists."))) + return + gui_app.push_widget(alert_dialog(tr("Backup creation started."))) + def _task(): + os.makedirs("/data/backups", exist_ok=True) + subprocess.run(["tar", "--use-compress-program=zstd", "-cf", backup_path, "/data/openpilot"]) + threading.Thread(target=_task, daemon=True).start() + self._keyboard.reset(min_text_size=0) + self._keyboard.set_title(tr("Name your backup"), "") + self._keyboard.set_text("") + self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text)) + gui_app.push_widget(self._keyboard) + + def _on_restore_backup(self): + backups = self._get_backups("backups") + if not backups: + gui_app.push_widget(alert_dialog(tr("No backups found."))) + return + dialog = MultiOptionDialog(tr("Select Backup"), backups) + def _on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + gui_app.push_widget(alert_dialog(tr("Restoring... device will reboot."))) + def _task(): + subprocess.run(["rm", "-rf", "/data/openpilot/*"]) + subprocess.run(["tar", "--use-compress-program=zstd", "-xf", f"/data/backups/{dialog.selection}", "-C", "/"]) + os.system("reboot") + threading.Thread(target=_task, daemon=True).start() + gui_app.push_widget(dialog, callback=_on_select) + + def _on_delete_backup(self): + backups = self._get_backups("backups") + if not backups: + gui_app.push_widget(alert_dialog(tr("No backups found."))) + return + dialog = MultiOptionDialog(tr("Delete Backup"), backups) + def _on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + os.remove(f"/data/backups/{dialog.selection}") + gui_app.push_widget(dialog, callback=_on_select) + + def _on_create_toggle_backup(self): + def on_name(res, name): + if res == DialogResult.CONFIRM: + safe_name = name.replace(" ", "_") if name else f"toggle_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + backup_path = Path(f"/data/toggle_backups/{safe_name}") + if backup_path.exists(): + gui_app.push_widget(alert_dialog(tr("A toggle backup with this name already exists."))) + return + os.makedirs(backup_path, exist_ok=True) + shutil.copytree("/data/params/d", str(backup_path), dirs_exist_ok=True) + gui_app.push_widget(alert_dialog(tr("Toggle backup created."))) + self._keyboard.reset(min_text_size=0) + self._keyboard.set_title(tr("Name your toggle backup"), "") + self._keyboard.set_text("") + self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text)) + gui_app.push_widget(self._keyboard) + + def _on_restore_toggle_backup(self): + backups = self._get_backups("toggle_backups") + if not backups: + gui_app.push_widget(alert_dialog(tr("No toggle backups found."))) + return + dialog = MultiOptionDialog(tr("Select Toggle Backup"), backups) + def _on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + def on_confirm(r2): + if r2 == DialogResult.CONFIRM: + src = Path(f"/data/toggle_backups/{dialog.selection}") + params_dir = Path("/data/params/d") + for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items(): + if (src / old_key).exists(): + (params_dir / new_key).unlink(missing_ok=True) + shutil.copytree(str(src), "/data/params/d", dirs_exist_ok=True) + for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items(): + old_path = params_dir / old_key + new_path = params_dir / new_key + if old_path.exists(): + old_path.replace(new_path) + gui_app.push_widget(alert_dialog(tr("Toggles restored."))) + gui_app.push_widget(ConfirmDialog(tr("This will overwrite your current toggles."), tr("Restore"), on_close=on_confirm)) + gui_app.push_widget(dialog, callback=_on_select) + + def _on_delete_toggle_backup(self): + backups = self._get_backups("toggle_backups") + if not backups: + gui_app.push_widget(alert_dialog(tr("No toggle backups found."))) + return + dialog = MultiOptionDialog(tr("Delete Toggle Backup"), backups) + def _on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + shutil.rmtree(f"/data/toggle_backups/{dialog.selection}", ignore_errors=True) + gui_app.push_widget(dialog, callback=_on_select) + + def _get_force_drive_state(self): + if self._params.get_bool("ForceOnroad"): + return tr("Onroad") + if self._params.get_bool("ForceOffroad"): + return tr("Offroad") + return tr("Default") + + def _on_flash_panda(self): + def _do_flash(res): + if res == DialogResult.CONFIRM: + self._params_memory.put_bool("FlashPanda", True) + gui_app.push_widget(alert_dialog(tr("Panda flashing started. Device will reboot when finished."))) + gui_app.push_widget(ConfirmDialog(tr("Flash Panda firmware?"), tr("Flash"), callback=_do_flash)) + + def _on_report_issue(self): + def on_category(res): + if res != DialogResult.CONFIRM or not dialog.selection: + return + discord_user = self._params.get("DiscordUsername", encoding='utf-8') or "" + def on_discord(res2, username): + if res2 == DialogResult.CONFIRM and username: + self._params.put("DiscordUsername", username) + report = json.dumps({"DiscordUser": username, "Issue": dialog.selection}) + self._params_memory.put("IssueReported", report) + gui_app.push_widget(alert_dialog(tr("Issue reported. Thank you!"))) + self._keyboard.reset(min_text_size=1) + self._keyboard.set_title(tr("Discord Username"), "") + self._keyboard.set_text(discord_user or "") + self._keyboard.set_callback(lambda result: on_discord(result, self._keyboard.text)) + gui_app.push_widget(self._keyboard) + dialog = MultiOptionDialog(tr("Select Issue"), REPORT_CATEGORIES, callback=on_category) + gui_app.push_widget(dialog) + + def _on_reset_defaults(self): + def _do_reset(res): + if res == DialogResult.CONFIRM: + all_keys = self._params.all_keys() + for k in all_keys: + if k in EXCLUDED_KEYS: + continue + default = self._params.get_default_value(k) + if default is not None: + self._params.put(k, default) + gui_app.push_widget(alert_dialog(tr("Toggles reset to defaults."))) + gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to defaults?"), tr("Reset"), callback=_do_reset)) + + def _on_reset_stock(self): + def _do_reset(res): + if res == DialogResult.CONFIRM: + all_keys = self._params.all_keys() + for k in all_keys: + if k in EXCLUDED_KEYS: + continue + stock = self._params.get_stock_value(k) + if stock is not None: + self._params.put(k, stock) + gui_app.push_widget(alert_dialog(tr("Toggles reset to stock openpilot."))) + gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to stock openpilot?"), tr("Reset"), callback=_do_reset)) diff --git a/selfdrive/ui/layouts/settings/starpilot/themes.py b/selfdrive/ui/layouts/settings/starpilot/themes.py index cd56bc459..fb1e6e11d 100644 --- a/selfdrive/ui/layouts/settings/starpilot/themes.py +++ b/selfdrive/ui/layouts/settings/starpilot/themes.py @@ -175,7 +175,7 @@ class StarPilotThemesLayout(StarPilotPanel): self._params.remove("StartupMessageTop") self._params.remove("StartupMessageBottom") - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) class StarPilotPersonalizeLayout(StarPilotPanel): @@ -329,4 +329,4 @@ class StarPilotPersonalizeLayout(StarPilotPanel): self._params.put(key, selected_slug) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) diff --git a/selfdrive/ui/layouts/settings/starpilot/utilities.py b/selfdrive/ui/layouts/settings/starpilot/utilities.py deleted file mode 100644 index 2436e38d6..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/utilities.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import annotations -import json -from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog -from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog -from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid - -EXCLUDED_KEYS = { - "AvailableModels", - "AvailableModelNames", - "StarPilotStats", - "GithubSshKeys", - "GithubUsername", - "MapBoxRequests", - "ModelDrivesAndScores", - "OverpassRequests", - "SpeedLimits", - "SpeedLimitsFiltered", - "UpdaterAvailableBranches", -} - -REPORT_CATEGORIES = [ - "Acceleration feels harsh or jerky", - "An alert was unclear and I'm not sure what it meant", - "Braking is too sudden or uncomfortable", - "I'm not sure if this is normal or a bug:", - "My steering wheel buttons aren't working", - "openpilot disengages when I don't expect it", - "openpilot feels sluggish or slow to respond", - "Something else (please describe)", -] - - -class StarPilotUtilitiesLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self._keyboard = Keyboard(min_text_size=1) - self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) - self.CATEGORIES = [ - { - "title": tr_noop("Debug Mode"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("DebugMode"), - "set_state": lambda s: self._params.put_bool("DebugMode", s), - "color": "#D43D8A", - }, - {"title": tr_noop("Flash Panda"), "type": "hub", "on_click": self._on_flash_panda, "color": "#D43D8A"}, - { - "title": tr_noop("Force Drive State"), - "type": "value", - "get_value": self._get_force_drive_state, - "on_click": self._on_force_drive_state, - "color": "#D43D8A", - }, - {"title": tr_noop("Report Issue"), "type": "hub", "on_click": self._on_report_issue, "color": "#D43D8A"}, - {"title": tr_noop("Reset to Defaults"), "type": "hub", "on_click": self._on_reset_defaults, "color": "#D43D8A"}, - {"title": tr_noop("Reset to Stock"), "type": "hub", "on_click": self._on_reset_stock, "color": "#D43D8A"}, - ] - self._rebuild_grid() - - def _get_force_drive_state(self): - if self._params.get_bool("ForceOnroad"): - return tr("Onroad") - if self._params.get_bool("ForceOffroad"): - return tr("Offroad") - return tr("Default") - - def _on_flash_panda(self): - def _do_flash(res): - if res == DialogResult.CONFIRM: - self._params_memory.put_bool("FlashPanda", True) - gui_app.push_widget(alert_dialog(tr("Panda flashing started. Device will reboot when finished."))) - - gui_app.push_widget(ConfirmDialog(tr("Flash Panda firmware?"), tr("Flash"), callback=_do_flash)) - - def _on_force_drive_state(self): - options = [tr("Offroad"), tr("Onroad"), tr("Default")] - current = self._get_force_drive_state() - def on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - if dialog.selection == tr("Offroad"): - self._params.put_bool("ForceOffroad", True) - self._params.put_bool("ForceOnroad", False) - elif dialog.selection == tr("Onroad"): - self._params.put_bool("ForceOnroad", True) - self._params.put_bool("ForceOffroad", False) - else: - self._params.put_bool("ForceOffroad", False) - self._params.put_bool("ForceOnroad", False) - self._rebuild_grid() - - dialog = MultiOptionDialog(tr("Force Drive State"), options, current, callback=on_select) - gui_app.push_widget(dialog) - - def _on_report_issue(self): - def on_category(res): - if res != DialogResult.CONFIRM or not dialog.selection: - return - discord_user = self._params.get("DiscordUsername", encoding='utf-8') or "" - - def on_discord(res2, username): - if res2 == DialogResult.CONFIRM and username: - self._params.put("DiscordUsername", username) - report = json.dumps({"DiscordUser": username, "Issue": dialog.selection}) - self._params_memory.put("IssueReported", report) - gui_app.push_widget(alert_dialog(tr("Issue reported. Thank you!"))) - - self._keyboard.reset(min_text_size=1) - self._keyboard.set_title(tr("Discord Username"), "") - self._keyboard.set_text(discord_user or "") - self._keyboard.set_callback(lambda result: on_discord(result, self._keyboard.text)) - gui_app.push_widget(self._keyboard) - - dialog = MultiOptionDialog(tr("Select Issue"), REPORT_CATEGORIES, callback=on_category) - gui_app.push_widget(dialog) - - def _on_reset_defaults(self): - def _do_reset(res): - if res == DialogResult.CONFIRM: - all_keys = self._params.all_keys() - for k in all_keys: - if k in EXCLUDED_KEYS: - continue - default = self._params.get_default_value(k) - if default is not None: - self._params.put(k, default) - gui_app.push_widget(alert_dialog(tr("Toggles reset to defaults."))) - self._rebuild_grid() - - gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to defaults?"), tr("Reset"), callback=_do_reset)) - - def _on_reset_stock(self): - def _do_reset(res): - if res == DialogResult.CONFIRM: - all_keys = self._params.all_keys() - for k in all_keys: - if k in EXCLUDED_KEYS: - continue - stock = self._params.get_stock_value(k) - if stock is not None: - self._params.put(k, stock) - gui_app.push_widget(alert_dialog(tr("Toggles reset to stock openpilot."))) - self._rebuild_grid() - - gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to stock openpilot?"), tr("Reset"), callback=_do_reset)) diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index 3459cf08c..476752b51 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -167,7 +167,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): def _on_select_make(self): makes = list(self._make_options) if not makes: - gui_app.set_modal_overlay(ConfirmDialog(tr("No fingerprint list available."), tr("OK"), on_close=lambda r: None)) + gui_app.push_widget(ConfirmDialog(tr("No fingerprint list available."), tr("OK"), on_close=lambda r: None)) return current_make = self._params.get("CarMake", encoding='utf-8') or "" @@ -185,17 +185,17 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): self._params.remove("CarModelName") self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) def _on_select_model(self): make = self._params.get("CarMake", encoding='utf-8') or "" if not make: - gui_app.set_modal_overlay(ConfirmDialog(tr("Please select a Car Make first!"), tr("OK"), on_close=lambda r: None)) + gui_app.push_widget(ConfirmDialog(tr("Please select a Car Make first!"), tr("OK"), on_close=lambda r: None)) return model_options = self._models_by_make.get(make, ()) if not model_options: - gui_app.set_modal_overlay(ConfirmDialog(tr("No models available for this make."), tr("OK"), on_close=lambda r: None)) + gui_app.push_widget(ConfirmDialog(tr("No models available for this make."), tr("OK"), on_close=lambda r: None)) return option_labels = [option.option_label for option in model_options] @@ -216,7 +216,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): self._params.put("CarMake", make) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) def _on_disable_long(self, state): if state: @@ -230,7 +230,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): HARDWARE.reboot() self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), on_close=on_confirm)) + gui_app.push_widget(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), on_close=on_confirm)) else: self._params.put_bool("DisableOpenpilotLongitudinal", False) self._rebuild_grid() @@ -447,7 +447,7 @@ class StarPilotToyotaVehicleLayout(StarPilotPanel): self._params.put_int("LockDoorsTimer", int(val)) self._rebuild_grid() - gui_app.set_modal_overlay( + gui_app.push_widget( AetherSliderDialog(tr("Lock Doors Timer"), 0, 300, 5, self._params.get_int("LockDoorsTimer"), on_close, labels=_lock_doors_timer_labels(), color="#64748B") ) @@ -457,7 +457,7 @@ class StarPilotToyotaVehicleLayout(StarPilotPanel): self._params.put_float("ClusterOffset", float(val)) self._rebuild_grid() - gui_app.set_modal_overlay( + gui_app.push_widget( AetherSliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, self._params.get_float("ClusterOffset"), on_close, unit="x", color="#64748B") ) diff --git a/selfdrive/ui/layouts/settings/starpilot/visuals.py b/selfdrive/ui/layouts/settings/starpilot/visuals.py index c9fa7030c..fdf9c17a8 100644 --- a/selfdrive/ui/layouts/settings/starpilot/visuals.py +++ b/selfdrive/ui/layouts/settings/starpilot/visuals.py @@ -439,7 +439,7 @@ class StarPilotModelUILayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#8B5CF6")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#8B5CF6")) def _show_float_selector(self, key, min_v, max_v, step, unit="", convert=None, unconvert=None): current = self._params.get_float(key) @@ -454,7 +454,7 @@ class StarPilotModelUILayout(StarPilotPanel): self._params.put_float(key, v) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#8B5CF6")) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#8B5CF6")) def _get_color_display(self, key): val = self._params.get(key, encoding='utf-8') or "" @@ -476,7 +476,7 @@ class StarPilotModelUILayout(StarPilotPanel): self._params.put(key, dialog.selection) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) class StarPilotNavigationVisualsLayout(StarPilotPanel): @@ -577,4 +577,4 @@ class StarPilotVisualQOLLayout(StarPilotPanel): self._params.put_int("CameraView", idx) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) diff --git a/selfdrive/ui/layouts/settings/starpilot/wheel.py b/selfdrive/ui/layouts/settings/starpilot/wheel.py index 3de516b63..81b4d9623 100644 --- a/selfdrive/ui/layouts/settings/starpilot/wheel.py +++ b/selfdrive/ui/layouts/settings/starpilot/wheel.py @@ -115,7 +115,7 @@ class StarPilotWheelLayout(StarPilotPanel): self._params_memory.put_bool("StarPilotTogglesUpdated", True) self._rebuild_grid() - gui_app.set_modal_overlay(dialog, callback=on_select) + gui_app.push_widget(dialog, callback=on_select) def _rebuild_grid(self): if not self.CATEGORIES: