From 37c82cc2d446958be5d807f38bcab8f6b2477ca6 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Sun, 3 May 2026 00:14:43 -0400 Subject: [PATCH] BigUI WIP: Start of lateral --- .../ui/layouts/settings/starpilot/lateral.py | 449 +++++++++--------- .../settings/starpilot/longitudinal.py | 71 +-- 2 files changed, 253 insertions(+), 267 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index 5ee5b87e2..a4bcee616 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -1,28 +1,45 @@ from __future__ import annotations + from openpilot.system.hardware import HARDWARE from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state 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, create_master_toggle_panel, create_tile_panel -from openpilot.selfdrive.ui.layouts.settings.starpilot.tabbed_panel import TabSectionSpec, TabbedSectionHost -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog -class StarPilotAdvancedLateralLayout(StarPilotPanel): +from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import _SettingsPage +from openpilot.selfdrive.ui.layouts.settings.starpilot.longitudinal import ( + SettingRow, SettingSection, AetherSettingsView, +) +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_LIST_METRICS, + AetherSliderDialog, + DEFAULT_PANEL_STYLE, +) + + +PANEL_STYLE = DEFAULT_PANEL_STYLE +UTILITY_ROW_HEIGHT = AETHER_LIST_METRICS.utility_row_height + + +def _confirm_reboot_toggle(params, key, state): + params.put_bool(key, state) + from openpilot.selfdrive.ui.ui_state import ui_state + if ui_state.started: + gui_app.push_widget(ConfirmDialog( + tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), + callback=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None, + )) + + +# ═══════════════════════════════════════════════════════════════ +# StarPilotAdvancedLateralLayout +# ═══════════════════════════════════════════════════════════════ + +class StarPilotAdvancedLateralLayout(_SettingsPage): def __init__(self): super().__init__() - self.CATEGORIES = [ - {"title": tr_noop("Actuator Delay"), "desc": tr_noop("The time between openpilot's steering command and the vehicle's response."), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerDelay'):.2f}s", "on_click": lambda: self._show_float_selector("SteerDelay", 0.01, 1.0, 0.01, "s"), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#597497", "visible": self._show_steer_delay}, - {"title": tr_noop("Friction"), "desc": tr_noop("Compensates for steering friction around center."), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerFriction'):.3f}", "on_click": self._show_steer_friction_selector, "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#597497", "visible": self._show_steer_friction}, - {"title": tr_noop("Kp Factor"), "desc": tr_noop("How strongly openpilot corrects lateral position."), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerKP'):.2f}", "on_click": self._show_steer_kp_selector, "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#597497", "visible": self._show_steer_kp}, - {"title": tr_noop("Lateral Acceleration"), "desc": tr_noop("Maps steering torque to turning response."), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerLatAccel'):.2f}", "on_click": self._show_steer_lat_accel_selector, "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#597497", "visible": self._show_steer_lat_accel}, - {"title": tr_noop("Steer Ratio"), "desc": tr_noop("Adjust the relationship between steering wheel input and road-wheel angle."), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerRatio'):.2f}", "on_click": self._show_steer_ratio_selector, "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#597497", "visible": self._show_steer_ratio}, - {"title": tr_noop("Force Auto-Tune On"), "desc": tr_noop("Force-enable live auto-tuning for friction and lateral acceleration."), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceAutoTune"), "set_state": self._set_force_auto_tune, "icon": "toggle_icons/icon_tuning.png", "color": "#597497", "visible": self._show_force_auto_tune}, - {"title": tr_noop("Force Auto-Tune Off"), "desc": tr_noop("Force-disable live auto-tuning and use your set values instead."), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceAutoTuneOff"), "set_state": self._set_force_auto_tune_off, "icon": "toggle_icons/icon_tuning.png", "color": "#597497", "visible": self._show_force_auto_tune_off}, - {"title": tr_noop("Force Torque Controller"), "desc": tr_noop("Use torque-based steering control instead of the stock steering mode when supported."), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceTorqueController"), "set_state": lambda x: self._on_reboot_toggle("ForceTorqueController", x), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#597497", "visible": self._show_force_torque_controller}, - ] - self._rebuild_grid() + self._build_view() def _advanced_enabled(self): return self._params.get_bool("AdvancedLateralTune") @@ -47,240 +64,198 @@ class StarPilotAdvancedLateralLayout(StarPilotPanel): return self._forcing_auto_tune_off() return not self._forcing_auto_tune() - def _show_steer_delay(self): - return self._advanced_enabled() and starpilot_state.car_state.steerActuatorDelay != 0 + def _build_view(self): + adv = self._advanced_enabled + torque = self._torque_tuning_active + manual = self._manual_tuning_values_enabled + nnff = self._using_nnff - def _show_steer_friction(self): - return (self._advanced_enabled() and starpilot_state.car_state.friction != 0 and self._torque_tuning_active() - and not self._using_nnff() and self._manual_tuning_values_enabled()) - - def _show_steer_kp(self): - return (self._advanced_enabled() and starpilot_state.car_state.steerKp != 0 and self._torque_tuning_active() - and not starpilot_state.car_state.isAngleCar) - - def _show_steer_lat_accel(self): - return (self._advanced_enabled() and starpilot_state.car_state.latAccelFactor != 0 and self._torque_tuning_active() - and not self._using_nnff() and self._manual_tuning_values_enabled()) - - def _show_steer_ratio(self): - return self._advanced_enabled() and starpilot_state.car_state.steerRatio != 0 and self._manual_tuning_values_enabled() - - def _show_force_auto_tune(self): - return (self._advanced_enabled() and not starpilot_state.car_state.hasAutoTune and not starpilot_state.car_state.isAngleCar - and self._torque_tuning_active()) - - def _show_force_auto_tune_off(self): - return self._advanced_enabled() and starpilot_state.car_state.hasAutoTune and not starpilot_state.car_state.isAngleCar - - def _show_force_torque_controller(self): - return self._advanced_enabled() and not starpilot_state.car_state.isAngleCar and not starpilot_state.car_state.isTorqueCar - - def _set_force_auto_tune(self, state): - self._params.put_bool("ForceAutoTune", state) - if state: - self._params.put_bool("ForceAutoTuneOff", False) - - def _set_force_auto_tune_off(self, state): - self._params.put_bool("ForceAutoTuneOff", state) - if state: - self._params.put_bool("ForceAutoTune", False) - - def _show_steer_friction_selector(self): - self._show_float_selector("SteerFriction", 0.0, max(1.0, starpilot_state.car_state.friction * 1.5), 0.01) - - def _show_steer_kp_selector(self): - base = max(0.01, starpilot_state.car_state.steerKp) - self._show_float_selector("SteerKP", base * 0.5, base * 1.5, 0.01) - - def _show_steer_lat_accel_selector(self): - base = max(0.01, starpilot_state.car_state.latAccelFactor) - self._show_float_selector("SteerLatAccel", base * 0.5, base * 1.5, 0.01) - - def _show_steer_ratio_selector(self): - base = max(0.01, starpilot_state.car_state.steerRatio) - self._show_float_selector("SteerRatio", base * 0.5, base * 1.5, 0.01) - - 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.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"), callback=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None) - gui_app.push_widget(dialog) - -class StarPilotAlwaysOnLateralLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - {"title": tr_noop("Always On Lateral"), "type": "toggle", "get_state": lambda: self._params.get_bool("AlwaysOnLateral"), "set_state": lambda x: self._on_reboot_toggle("AlwaysOnLateral", x), "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#597497"}, - {"title": tr_noop("Enable With LKAS"), "type": "toggle", "get_state": lambda: self._params.get_bool("AlwaysOnLateralLKAS"), "set_state": lambda x: self._params.put_bool("AlwaysOnLateralLKAS", x), "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#597497", "visible": lambda: self._params.get_bool("AlwaysOnLateral") and starpilot_state.car_state.lkasAllowedForAOL}, - {"title": tr_noop("Pause Below"), "type": "value", "get_value": lambda: f"{self._params.get_int('PauseAOLOnBrake')} mph", "on_click": lambda: self._show_speed_selector("PauseAOLOnBrake"), "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#597497", "visible": lambda: self._params.get_bool("AlwaysOnLateral")}, + sections = [ + SettingSection(tr_noop("Steering Tuning"), [ + SettingRow("SteerDelay", "value", tr_noop("Actuator Delay"), + subtitle=tr_noop("The time between openpilot's steering command and the vehicle's response."), + get_value=lambda: f"{self._params.get_float('SteerDelay'):.2f}s", + on_click=lambda: self._show_slider("SteerDelay", 0.01, 1.0, step=0.01, unit="s", value_type="float"), + visible=lambda: adv() and starpilot_state.car_state.steerActuatorDelay != 0), + SettingRow("SteerFriction", "value", tr_noop("Friction"), + subtitle=tr_noop("Compensates for steering friction around center."), + get_value=lambda: f"{self._params.get_float('SteerFriction'):.3f}", + on_click=lambda: self._show_slider("SteerFriction", 0.0, max(1.0, starpilot_state.car_state.friction * 1.5), step=0.01, value_type="float"), + visible=lambda: adv() and starpilot_state.car_state.friction != 0 and torque() and not nnff() and manual()), + SettingRow("SteerKP", "value", tr_noop("Kp Factor"), + subtitle=tr_noop("How strongly openpilot corrects lateral position."), + get_value=lambda: f"{self._params.get_float('SteerKP'):.2f}", + on_click=lambda: self._show_slider("SteerKP", max(0.01, starpilot_state.car_state.steerKp) * 0.5, max(0.01, starpilot_state.car_state.steerKp) * 1.5, step=0.01, value_type="float"), + visible=lambda: adv() and starpilot_state.car_state.steerKp != 0 and torque() and not starpilot_state.car_state.isAngleCar), + SettingRow("SteerLatAccel", "value", tr_noop("Lateral Acceleration"), + subtitle=tr_noop("Maps steering torque to turning response."), + get_value=lambda: f"{self._params.get_float('SteerLatAccel'):.2f}", + on_click=lambda: self._show_slider("SteerLatAccel", max(0.01, starpilot_state.car_state.latAccelFactor) * 0.5, max(0.01, starpilot_state.car_state.latAccelFactor) * 1.5, step=0.01, value_type="float"), + visible=lambda: adv() and starpilot_state.car_state.latAccelFactor != 0 and torque() and not nnff() and manual()), + SettingRow("SteerRatio", "value", tr_noop("Steer Ratio"), + subtitle=tr_noop("Adjust the relationship between steering wheel input and road-wheel angle."), + get_value=lambda: f"{self._params.get_float('SteerRatio'):.2f}", + on_click=lambda: self._show_slider("SteerRatio", max(0.01, starpilot_state.car_state.steerRatio) * 0.5, max(0.01, starpilot_state.car_state.steerRatio) * 1.5, step=0.01, value_type="float"), + visible=lambda: adv() and starpilot_state.car_state.steerRatio != 0 and manual()), + SettingRow("ForceAutoTune", "toggle", tr_noop("Force Auto-Tune On"), + subtitle=tr_noop("Force-enable live auto-tuning for friction and lateral acceleration."), + get_state=lambda: self._params.get_bool("ForceAutoTune"), + set_state=lambda s: (self._params.put_bool("ForceAutoTune", s), s and self._params.put_bool("ForceAutoTuneOff", False)), + visible=lambda: adv() and not starpilot_state.car_state.hasAutoTune and not starpilot_state.car_state.isAngleCar and torque()), + SettingRow("ForceAutoTuneOff", "toggle", tr_noop("Force Auto-Tune Off"), + subtitle=tr_noop("Force-disable live auto-tuning and use your set values instead."), + get_state=lambda: self._params.get_bool("ForceAutoTuneOff"), + set_state=lambda s: (self._params.put_bool("ForceAutoTuneOff", s), s and self._params.put_bool("ForceAutoTune", False)), + visible=lambda: adv() and starpilot_state.car_state.hasAutoTune and not starpilot_state.car_state.isAngleCar), + SettingRow("ForceTorqueController", "toggle", tr_noop("Force Torque Controller"), + subtitle=tr_noop("Use torque-based steering control instead of the stock steering mode when supported."), + get_state=lambda: self._params.get_bool("ForceTorqueController"), + set_state=lambda s: _confirm_reboot_toggle(self._params, "ForceTorqueController", s), + visible=lambda: adv() and not starpilot_state.car_state.isAngleCar and not starpilot_state.car_state.isTorqueCar), + ], row_height=UTILITY_ROW_HEIGHT), ] - self._rebuild_grid() + self._manager_view = AetherSettingsView( + self, sections, + header_title=tr_noop("Advanced Lateral Tuning"), + header_subtitle=tr_noop("Adjust steering response, torque controller behavior, and auto-tuning controls."), + panel_style=PANEL_STYLE, + ) - def _show_speed_selector(self, key): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(key, int(val)) - self._rebuild_grid() - 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.push_widget(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), callback=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)) +# ═══════════════════════════════════════════════════════════════ +# StarPilotLateralLayout — top-level hub with 3 tabs +# ═══════════════════════════════════════════════════════════════ -class StarPilotLaneChangesLayout(StarPilotPanel): +class StarPilotLateralLayout(_SettingsPage): def __init__(self): super().__init__() - self.CATEGORIES = [ - {"title": tr_noop("Lane Changes"), "type": "toggle", "get_state": lambda: self._params.get_bool("LaneChanges"), "set_state": lambda s: self._params.put_bool("LaneChanges", s), "icon": "toggle_icons/icon_lane.png", "color": "#597497"}, - {"title": tr_noop("Automatic Lane Changes"), "type": "toggle", "get_state": lambda: self._params.get_bool("NudgelessLaneChange"), "set_state": lambda s: self._params.put_bool("NudgelessLaneChange", s), "icon": "toggle_icons/icon_lane.png", "color": "#597497", "visible": lambda: self._params.get_bool("LaneChanges")}, - {"title": tr_noop("Lane Change Delay"), "type": "value", "get_value": lambda: f"{self._params.get_float('LaneChangeTime'):.1f}s", "on_click": lambda: self._show_float_selector("LaneChangeTime", 0.0, 5.0, 0.1, "s"), "icon": "toggle_icons/icon_lane.png", "color": "#597497", "visible": lambda: self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange")}, - {"title": tr_noop("Min Lane Change Speed"), "type": "value", "get_value": lambda: f"{self._params.get_int('MinimumLaneChangeSpeed')} mph", "on_click": lambda: self._show_speed_selector("MinimumLaneChangeSpeed"), "icon": "toggle_icons/icon_lane.png", "color": "#597497", "visible": lambda: self._params.get_bool("LaneChanges")}, - {"title": tr_noop("Minimum Lane Width"), "type": "value", "get_value": lambda: f"{self._params.get_float('LaneDetectionWidth'):.1f} ft", "on_click": lambda: self._show_float_selector("LaneDetectionWidth", 0.0, 15.0, 0.1, " ft"), "icon": "toggle_icons/icon_lane.png", "color": "#597497", "visible": lambda: self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange")}, - {"title": tr_noop("One Lane Change Per Signal"), "type": "toggle", "get_state": lambda: self._params.get_bool("OneLaneChange"), "set_state": lambda s: self._params.put_bool("OneLaneChange", s), "icon": "toggle_icons/icon_lane.png", "color": "#597497", "visible": lambda: self._params.get_bool("LaneChanges")}, - {"title": tr_noop("Lane Change Smoothing"), "desc": tr_noop("How smoothly openpilot commits to a lane change. 10 is stock; lower values produce a gentler, more gradual maneuver."), "type": "value", "get_value": lambda: f"{self._params.get_int('LaneChangeSmoothing')}", "on_click": lambda: self._show_pace_selector("LaneChangeSmoothing"), "icon": "toggle_icons/icon_lane.png", "color": "#597497", "visible": lambda: self._params.get_bool("LaneChanges")}, - ] - self._rebuild_grid() - def _show_speed_selector(self, key): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(key, int(val)) - self._rebuild_grid() - 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.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497")) - - def _show_pace_selector(self, key): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(key, int(val)) - self._rebuild_grid() - current = self._params.get_int(key) if self._params.get_int(key) > 0 else 10 - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 1, 10, 1, current, on_close, color="#597497")) - -class StarPilotLateralTuneLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - {"title": tr_noop("Force Turn Desires Below Lane Change Speed"), "desc": tr_noop("Allow openpilot to follow turn intent below the minimum lane change speed when signaling."), "type": "toggle", "get_state": lambda: self._params.get_bool("TurnDesires"), "set_state": lambda x: self._params.put_bool("TurnDesires", x), "icon": "toggle_icons/icon_lateral_tune.png", "color": "#597497", "visible": self._lateral_enabled}, - {"title": tr_noop("Neural Network Feedforward (NNFF)"), "desc": tr_noop("Use the full neural-network feedforward steering controller when available."), "type": "toggle", "get_state": lambda: self._params.get_bool("NNFF"), "set_state": self._set_nnff, "icon": "toggle_icons/icon_lateral_tune.png", "color": "#597497", "visible": self._show_nnff}, - {"title": tr_noop("Neural Network Feedforward (NNFF) Lite"), "desc": tr_noop("Use the lightweight NNFF steering logic when the full model is off."), "type": "toggle", "get_state": lambda: self._params.get_bool("NNFFLite"), "set_state": lambda x: self._on_reboot_toggle("NNFFLite", x), "icon": "toggle_icons/icon_lateral_tune.png", "color": "#597497", "visible": self._show_nnff_lite}, - ] - self._rebuild_grid() - - def _lateral_enabled(self): - return self._params.get_bool("LateralTune") - - def _show_nnff(self): - return self._lateral_enabled() and starpilot_state.car_state.hasNNFFLog and not starpilot_state.car_state.isAngleCar - - def _show_nnff_lite(self): - return self._lateral_enabled() and not self._params.get_bool("NNFF") and not starpilot_state.car_state.isAngleCar - - def _set_nnff(self, state): - self._params.put_bool("NNFF", state) - if state: - self._params.put_bool("NNFFLite", False) - - 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.push_widget(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), callback=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)) - -class StarPilotLateralQOLLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - {"title": tr_noop("Quality of Life"), "type": "toggle", "get_state": lambda: self._params.get_bool("QOLLateral"), "set_state": lambda x: self._params.put_bool("QOLLateral", x), "icon": "toggle_icons/icon_quality_of_life.png", "color": "#597497"}, - {"title": tr_noop("Pause Steering Below"), "type": "value", "get_value": lambda: f"{self._params.get_int('PauseLateralSpeed')} mph", "on_click": lambda: self._show_speed_selector("PauseLateralSpeed"), "icon": "toggle_icons/icon_quality_of_life.png", "color": "#597497", "visible": lambda: self._params.get_bool("QOLLateral")} - ] - self._rebuild_grid() - - def _show_speed_selector(self, key): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(key, int(val)) - self._rebuild_grid() - 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): - super().__init__() - steering_panel = create_tile_panel([ - {"title": tr_noop("Always On Lateral"), "panel": "always_on_lateral", "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#597497"}, - {"title": tr_noop("Quality of Life"), "panel": "qol", "icon": "toggle_icons/icon_quality_of_life.png", "color": "#597497"}, - ], { - "always_on_lateral": StarPilotAlwaysOnLateralLayout(), - "qol": StarPilotLateralQOLLayout(), - }) - tune_panel = create_master_toggle_panel([ - { - "title": tr_noop("Advanced Lateral Tuning"), - "desc": tr_noop("Advanced steering control changes to fine-tune how openpilot drives."), - "manage_title": tr_noop("Advanced Settings"), - "manage_desc": tr_noop("Adjust steering response, torque controller behavior, and auto-tuning controls."), - "manage_label": tr_noop("Configure"), - "disabled_label": tr_noop("Enable First"), - "panel": "advanced_lateral", - "get_state": lambda: self._params.get_bool("AdvancedLateralTune"), - "set_state": lambda x: self._params.put_bool("AdvancedLateralTune", x), - "icon": "toggle_icons/icon_advanced_lateral_tune.png", - "color": "#597497", - }, - { - "title": tr_noop("Lateral Tuning"), - "desc": tr_noop("Miscellaneous steering control changes such as turn desires and NNFF modes."), - "manage_title": tr_noop("Lateral Settings"), - "manage_desc": tr_noop("Open turn-intent and neural-network feedforward controls."), - "manage_label": tr_noop("Configure"), - "disabled_label": tr_noop("Enable First"), - "panel": "lateral_tune", - "get_state": lambda: self._params.get_bool("LateralTune"), - "set_state": lambda x: self._params.put_bool("LateralTune", x), - "icon": "toggle_icons/icon_lateral_tune.png", - "color": "#597497", - }, - ], { + self._sub_panels = { "advanced_lateral": StarPilotAdvancedLateralLayout(), - "lateral_tune": StarPilotLateralTuneLayout(), - }) + } + self._wire_sub_panels() + self._build_view() - self._section_tabs = TabbedSectionHost([ - TabSectionSpec("steering", "Steering", steering_panel), - TabSectionSpec("lane", "Lane", StarPilotLaneChangesLayout()), - TabSectionSpec("tune", "Tune", tune_panel), - ]) + def _build_view(self): + tab_defs = [ + {"id": "steering", "title": tr_noop("Steering"), "subtitle": tr_noop("Steering modes")}, + {"id": "lane", "title": tr_noop("Lane"), "subtitle": tr_noop("Lane changes")}, + {"id": "tune", "title": tr_noop("Tune"), "subtitle": tr_noop("Advanced controls")}, + ] - def set_navigate_callback(self, callback): - self._section_tabs.set_navigate_callback(callback) + sections = [ + # ── Steering tab ── + SettingSection(tr_noop("Steering Modes"), [ + SettingRow("AlwaysOnLateral", "toggle", tr_noop("Always On Lateral"), + subtitle=tr_noop("Keep lateral control active even without openpilot engaged."), + get_state=lambda: self._params.get_bool("AlwaysOnLateral"), + set_state=lambda s: _confirm_reboot_toggle(self._params, "AlwaysOnLateral", s)), + SettingRow("AlwaysOnLateralLKAS", "toggle", tr_noop("Enable With LKAS"), + subtitle="", + get_state=lambda: self._params.get_bool("AlwaysOnLateralLKAS"), + set_state=lambda s: self._params.put_bool("AlwaysOnLateralLKAS", s), + visible=lambda: self._params.get_bool("AlwaysOnLateral") and starpilot_state.car_state.lkasAllowedForAOL), + SettingRow("PauseAOLOnBrake", "value", tr_noop("Pause Below"), + subtitle="", + get_value=lambda: f"{self._params.get_int('PauseAOLOnBrake')} mph", + on_click=lambda: self._show_slider("PauseAOLOnBrake", 0, 100, unit=" mph"), + visible=lambda: self._params.get_bool("AlwaysOnLateral")), + SettingRow("QOLLateral", "toggle", tr_noop("Quality of Life"), + subtitle="", + get_state=lambda: self._params.get_bool("QOLLateral"), + set_state=lambda s: self._params.put_bool("QOLLateral", s)), + SettingRow("PauseLateralSpeed", "value", tr_noop("Pause Steering Below"), + subtitle="", + get_value=lambda: f"{self._params.get_int('PauseLateralSpeed')} mph", + on_click=lambda: self._show_slider("PauseLateralSpeed", 0, 100, unit=" mph"), + visible=lambda: self._params.get_bool("QOLLateral")), + ], tab_key="steering", row_height=UTILITY_ROW_HEIGHT), - def set_back_callback(self, callback): - self._section_tabs.set_back_callback(callback) + # ── Lane tab ── + SettingSection("", [ + SettingRow("LaneChanges", "toggle", tr_noop("Lane Changes"), + subtitle="", + get_state=lambda: self._params.get_bool("LaneChanges"), + set_state=lambda s: self._params.put_bool("LaneChanges", s)), + SettingRow("NudgelessLaneChange", "toggle", tr_noop("Automatic Lane Changes"), + subtitle="", + get_state=lambda: self._params.get_bool("NudgelessLaneChange"), + set_state=lambda s: self._params.put_bool("NudgelessLaneChange", s), + visible=lambda: self._params.get_bool("LaneChanges")), + SettingRow("LaneChangeTime", "value", tr_noop("Lane Change Delay"), + subtitle="", + get_value=lambda: f"{self._params.get_float('LaneChangeTime'):.1f}s", + on_click=lambda: self._show_slider("LaneChangeTime", 0.0, 5.0, step=0.1, unit="s", value_type="float"), + visible=lambda: self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange")), + SettingRow("MinimumLaneChangeSpeed", "value", tr_noop("Min Lane Change Speed"), + subtitle="", + get_value=lambda: f"{self._params.get_int('MinimumLaneChangeSpeed')} mph", + on_click=lambda: self._show_slider("MinimumLaneChangeSpeed", 0, 100, unit=" mph"), + visible=lambda: self._params.get_bool("LaneChanges")), + SettingRow("LaneDetectionWidth", "value", tr_noop("Minimum Lane Width"), + subtitle="", + get_value=lambda: f"{self._params.get_float('LaneDetectionWidth'):.1f} ft", + on_click=lambda: self._show_slider("LaneDetectionWidth", 0.0, 15.0, step=0.1, unit=" ft", value_type="float"), + visible=lambda: self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange")), + SettingRow("OneLaneChange", "toggle", tr_noop("One Lane Change Per Signal"), + subtitle="", + get_state=lambda: self._params.get_bool("OneLaneChange"), + set_state=lambda s: self._params.put_bool("OneLaneChange", s), + visible=lambda: self._params.get_bool("LaneChanges")), + SettingRow("LaneChangeSmoothing", "value", tr_noop("Lane Change Smoothing"), + subtitle=tr_noop("How smoothly openpilot commits to a lane change. 10 is stock; lower values produce a gentler, more gradual maneuver."), + get_value=lambda: f"{self._params.get_int('LaneChangeSmoothing')}", + on_click=self._show_modal_pace_selector, + visible=lambda: self._params.get_bool("LaneChanges")), + ], tab_key="lane", row_height=UTILITY_ROW_HEIGHT), - def _render(self, rect): - self._section_tabs.render(rect) + # ── Tune tab ── + SettingSection(tr_noop("Advanced Lateral Tuning"), [ + SettingRow("AdvancedLateralTune", "toggle", tr_noop("Advanced Lateral Tuning"), + subtitle=tr_noop("Advanced steering control changes to fine-tune how openpilot drives."), + get_state=lambda: self._params.get_bool("AdvancedLateralTune"), + set_state=lambda s: self._params.put_bool("AdvancedLateralTune", s)), + SettingRow("AdvancedConfigure", "value", tr_noop("Configure"), + subtitle=tr_noop("Adjust steering response, torque controller behavior, and auto-tuning controls."), + get_value=lambda: tr_noop("Settings"), + navigate_to="advanced_lateral", + enabled=lambda: self._params.get_bool("AdvancedLateralTune"), + disabled_label=tr_noop("Enable First")), + ], tab_key="tune", row_height=UTILITY_ROW_HEIGHT), + SettingSection(tr_noop("Lateral Tuning"), [ + SettingRow("LateralTune", "toggle", tr_noop("Lateral Tuning"), + subtitle=tr_noop("Miscellaneous steering control changes such as turn desires and NNFF modes."), + get_state=lambda: self._params.get_bool("LateralTune"), + set_state=lambda s: self._params.put_bool("LateralTune", s)), + SettingRow("TurnDesires", "toggle", tr_noop("Force Turn Desires Below Lane Change Speed"), + subtitle=tr_noop("Allow openpilot to follow turn intent below the minimum lane change speed when signaling."), + get_state=lambda: self._params.get_bool("TurnDesires"), + set_state=lambda s: self._params.put_bool("TurnDesires", s), + visible=lambda: self._params.get_bool("LateralTune")), + SettingRow("NNFF", "toggle", tr_noop("Neural Network Feedforward (NNFF)"), + subtitle=tr_noop("Use the full neural-network feedforward steering controller when available."), + get_state=lambda: self._params.get_bool("NNFF"), + set_state=lambda s: (self._params.put_bool("NNFF", s), s and self._params.put_bool("NNFFLite", False)), + visible=lambda: self._params.get_bool("LateralTune") and starpilot_state.car_state.hasNNFFLog and not starpilot_state.car_state.isAngleCar), + SettingRow("NNFFLite", "toggle", tr_noop("Neural Network Feedforward (NNFF) Lite"), + subtitle=tr_noop("Use the lightweight NNFF steering logic when the full model is off."), + get_state=lambda: self._params.get_bool("NNFFLite"), + set_state=lambda s: _confirm_reboot_toggle(self._params, "NNFFLite", s), + visible=lambda: self._params.get_bool("LateralTune") and not self._params.get_bool("NNFF") and not starpilot_state.car_state.isAngleCar), + ], tab_key="tune", row_height=UTILITY_ROW_HEIGHT), + ] - def set_current_sub_panel(self, sub_panel: str): - self._section_tabs.set_current_sub_panel(sub_panel) + self._manager_view = AetherSettingsView( + self, sections, + header_title=tr_noop("Steering"), + header_subtitle=tr_noop("Fine-tune lateral control, lane changes, and steering behavior."), + tab_defs=tab_defs, + panel_style=PANEL_STYLE, + ) - def show_event(self): - self._section_tabs.show_event() - - def hide_event(self): - self._section_tabs.hide_event() + def _show_modal_pace_selector(self): + def on_close(res, val): + if res == DialogResult.CONFIRM: + self._params.put_int("LaneChangeSmoothing", int(val)) + current = self._params.get_int("LaneChangeSmoothing") if self._params.get_int("LaneChangeSmoothing") > 0 else 10 + gui_app.set_modal_overlay(AetherSliderDialog(tr("Lane Change Smoothing"), 1, 10, 1, current, on_close, color="#597497")) diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py index 42dbdc898..f4e214250 100644 --- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py +++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py @@ -102,8 +102,11 @@ class AetherSettingsView(Widget): def __init__(self, controller: StarPilotPanel, sections: list[SettingSection], *, header_title: str = "", header_subtitle: str = "", - tab_defs: list[dict] | None = None): + tab_defs: list[dict] | None = None, + panel_style=None, fade_height: float = AETHER_LIST_METRICS.fade_height): super().__init__() + self._panel_style = panel_style or PANEL_STYLE + self._fade_height = fade_height self._controller = controller self._sections = sections self._header_title = header_title @@ -186,7 +189,7 @@ class AetherSettingsView(Widget): self.set_rect(rect) self._interactive_rects.clear() - frame, scroll_rect, content_width = init_list_panel(rect, PANEL_STYLE) + frame, scroll_rect, content_width = init_list_panel(rect, self._panel_style) self._scroll_rect = scroll_rect if self._has_header: @@ -206,7 +209,7 @@ class AetherSettingsView(Widget): self._scrollbar.render(self._scroll_rect, self._content_height, self._scroll_offset) draw_list_scroll_fades(self._scroll_rect, self._content_height, self._scroll_offset, - AetherListColors.PANEL_BG, fade_height=FADE_HEIGHT) + AetherListColors.PANEL_BG, fade_height=self._fade_height) def _draw_header(self, rect: rl.Rectangle): title = tr(self._header_title) if self._header_title else "" @@ -242,7 +245,7 @@ class AetherSettingsView(Widget): i += 2 else: i += 1 - total += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + total += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP if section.title else 0.0 total += row_h total += SECTION_GAP return max(0.0, total - SECTION_GAP) if total > 0 else 0.0 @@ -250,9 +253,8 @@ class AetherSettingsView(Widget): def _draw_tabs(self, y: float, x: float, width: float) -> float: if not self._tab_defs: return y - content_w = width - AETHER_LIST_METRICS.content_right_gutter n = len(self._tab_defs) - tab_w = (content_w - self.TAB_GAP * max(0, n - 1)) / max(1, n) + tab_w = (width - self.TAB_GAP * max(0, n - 1)) / max(1, n) for i, tab in enumerate(self._tab_defs): tab_rect = rl.Rectangle(x + i * (tab_w + self.TAB_GAP), y, tab_w, self.TAB_HEIGHT) target_id = f"tab:{tab['id']}" @@ -267,15 +269,20 @@ class AetherSettingsView(Widget): title_size=24, subtitle_size=17, show_underline=True, - style=PANEL_STYLE, + style=self._panel_style, ) return y + self.TAB_HEIGHT + self.TAB_BOTTOM_GAP + def _has_subsequent_visible(self, start_idx: int, sections: list[SettingSection]) -> bool: + for j in range(start_idx, len(sections)): + if self._visible_rows(sections[j]): + return True + return False + def _draw_scroll_content(self, rect: rl.Rectangle, width: float): y = rect.y + self._scroll_offset if self._tab_defs: y = self._draw_tabs(y, rect.x, width) - content_w = width - AETHER_LIST_METRICS.content_right_gutter active = self._active_sections() i = 0 while i < len(active): @@ -287,55 +294,59 @@ class AetherSettingsView(Widget): if section.column_pair and i + 1 < len(active) and active[i + 1].column_pair == section.column_pair: right_section = active[i + 1] right_rows = self._visible_rows(right_section) - col_w = (content_w - self.COLUMN_GAP) / 2 + col_w = (width - self.COLUMN_GAP) / 2 section_h = len(visible_rows) * section.row_height right_h = len(right_rows) * right_section.row_height group_h = max(section_h, right_h) draw_section_header( rl.Rectangle(rect.x, y, col_w, SECTION_HEADER_HEIGHT), - tr(section.title), style=PANEL_STYLE, + tr(section.title), style=self._panel_style, ) draw_section_header( rl.Rectangle(rect.x + col_w + self.COLUMN_GAP, y, col_w, SECTION_HEADER_HEIGHT), - tr(right_section.title), style=PANEL_STYLE, + tr(right_section.title), style=self._panel_style, ) y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP left_group = rl.Rectangle(rect.x, y, col_w, section_h) right_group = rl.Rectangle(rect.x + col_w + self.COLUMN_GAP, y, col_w, right_h) - draw_list_group_shell(left_group, style=PANEL_STYLE) - draw_list_group_shell(right_group, style=PANEL_STYLE) + draw_list_group_shell(left_group, style=self._panel_style) + draw_list_group_shell(right_group, style=self._panel_style) for j, row in enumerate(visible_rows): self._draw_row(rl.Rectangle(rect.x, y + j * section.row_height, col_w, section.row_height), row, is_last=(j == len(visible_rows) - 1)) for j, row in enumerate(right_rows): self._draw_row(rl.Rectangle(rect.x + col_w + self.COLUMN_GAP, y + j * right_section.row_height, col_w, right_section.row_height), row, is_last=(j == len(right_rows) - 1)) - y += group_h + SECTION_GAP + y += group_h + if self._has_subsequent_visible(i + 2, active): + y += SECTION_GAP i += 2 else: y = self._draw_section(y, rect.x, width, section, visible_rows) + if self._has_subsequent_visible(i + 1, active): + y += SECTION_GAP i += 1 def _draw_section(self, y: float, x: float, width: float, section: SettingSection, rows: list[SettingRow]) -> float: - content_w = width - AETHER_LIST_METRICS.content_right_gutter - draw_section_header( - rl.Rectangle(x, y, content_w, SECTION_HEADER_HEIGHT), - tr(section.title), - style=PANEL_STYLE, - ) - y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + if section.title: + draw_section_header( + rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), + tr(section.title), + style=self._panel_style, + ) + y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - group_rect = rl.Rectangle(x, y, content_w, len(rows) * section.row_height) - draw_list_group_shell(group_rect, style=PANEL_STYLE) + group_rect = rl.Rectangle(x, y, width, len(rows) * section.row_height) + draw_list_group_shell(group_rect, style=self._panel_style) for i, row in enumerate(rows): - row_rect = rl.Rectangle(x, y + i * section.row_height, content_w, section.row_height) + row_rect = rl.Rectangle(x, y + i * section.row_height, width, section.row_height) self._draw_row(row_rect, row, is_last=(i == len(rows) - 1)) - return y + group_rect.height + SECTION_GAP + return y + group_rect.height def _draw_row(self, rect: rl.Rectangle, row: SettingRow, is_last: bool): target_id = f"{row.type}:{row.id}" @@ -356,7 +367,7 @@ class AetherSettingsView(Widget): pressed=pressed, is_last=is_last, show_chevron=False, - style=PANEL_STYLE, + style=self._panel_style, ) elif row.type == "value": value_text = row.get_value() if row.get_value else "" @@ -370,13 +381,13 @@ class AetherSettingsView(Widget): pressed=pressed, is_last=is_last, show_chevron=row.on_click is not None, - style=PANEL_STYLE, + style=self._panel_style, ) elif row.type == "action": - action_fill = AetherListColors.DANGER_SOFT if row.action_danger else PANEL_STYLE.current_fill + action_fill = AetherListColors.DANGER_SOFT if row.action_danger else self._panel_style.current_fill action_border = (rl.Color(AetherListColors.DANGER.r, AetherListColors.DANGER.g, AetherListColors.DANGER.b, 70) - if row.action_danger else PANEL_STYLE.current_border) + if row.action_danger else self._panel_style.current_border) action_text_color = AetherListColors.DANGER if row.action_danger else AetherListColors.HEADER draw_selection_list_row( rect, @@ -390,7 +401,7 @@ class AetherSettingsView(Widget): action_text_color=action_text_color, action_fill=action_fill, action_border=action_border, - row_separator=PANEL_STYLE.divider_color, + row_separator=self._panel_style.divider_color, )