BigUI WIP: Start of lateral

This commit is contained in:
firestarsdog
2026-05-03 00:14:43 -04:00
parent 1c38b731bf
commit 37c82cc2d4
2 changed files with 253 additions and 267 deletions
+212 -237
View File
@@ -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"))
@@ -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,
)