BigUI WIP: Lateral Spateral

This commit is contained in:
firestarsdog
2026-06-09 13:13:55 -04:00
parent 6faf9bd88a
commit 4f48c2ec27

View File

@@ -1,26 +1,31 @@
from __future__ import annotations
import pyray as rl
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.lib.multilang import tr
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 _SettingsPage
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
SettingRow, SettingSection, AetherSettingsView, COMPACT_PANEL_METRICS,
)
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AETHER_LIST_METRICS,
AetherListMetrics,
AetherListColors,
AetherSliderDialog,
DEFAULT_PANEL_STYLE,
PanelManagerView,
TileGrid,
ToggleTile,
draw_list_group_shell,
draw_section_header,
draw_selection_list_row,
draw_settings_panel_header,
_with_alpha,
)
PANEL_STYLE = DEFAULT_PANEL_STYLE
def _confirm_reboot_toggle(params, key, state):
params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
@@ -31,242 +36,543 @@ def _confirm_reboot_toggle(params, key, state):
))
# ═══════════════════════════════════════════════════════════════
# StarPilotAdvancedLateralLayout
# ═══════════════════════════════════════════════════════════════
CUSTOM_METRICS = AetherListMetrics(
max_content_width=1560,
outer_margin_x=18,
outer_margin_y=10,
panel_padding_x=16,
panel_padding_top=16,
panel_padding_bottom=12,
header_height=164,
section_gap=12,
section_header_height=28,
section_header_gap=8,
row_height=104,
utility_row_height=88,
)
class StarPilotAdvancedLateralLayout(_SettingsPage):
def __init__(self):
SECTION_GAP = CUSTOM_METRICS.section_gap
SECTION_HEADER_HEIGHT = CUSTOM_METRICS.section_header_height
SECTION_HEADER_GAP = CUSTOM_METRICS.section_header_gap
ROW_HEIGHT = CUSTOM_METRICS.row_height
PANEL_STYLE = DEFAULT_PANEL_STYLE
_LATERAL_TUNE_KEYS = ["TurnDesires", "NNFF", "NNFFLite", "ForceTorqueController"]
_ADVANCED_LATERAL_KEYS = ["ForceAutoTune", "ForceAutoTuneOff"]
def _ensure(params, key):
if not params.get_bool(key):
params.put_bool(key, True)
def _sync_parent(params, parent_key, child_keys):
if any(params.get_bool(k) for k in child_keys):
if not params.get_bool(parent_key):
params.put_bool(parent_key, True)
else:
if params.get_bool(parent_key):
params.put_bool(parent_key, False)
class SteeringManagerView(PanelManagerView):
METRICS = CUSTOM_METRICS
def __init__(self, controller: "StarPilotLateralLayout"):
super().__init__()
self._build_view()
self._controller = controller
self._shell_rect = rl.Rectangle(0, 0, 0, 0)
def _advanced_enabled(self):
return self._params.get_bool("AdvancedLateralTune")
self._toggle_grid = TileGrid(columns=2, padding=12, min_tile_width=100)
self._toggle_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
self._child(self._toggle_grid)
def _using_nnff(self):
return starpilot_state.car_state.hasNNFFLog and self._params.get_bool("LateralTune") and self._params.get_bool("NNFF")
def show_event(self):
super().show_event()
starpilot_state.update(force=True)
self._rebuild_toggle_grid()
def _forcing_auto_tune(self):
return not starpilot_state.car_state.hasAutoTune and self._params.get_bool("ForceAutoTune")
def _on_frame_created(self, frame) -> None:
self._shell_rect = frame.shell
def _forcing_auto_tune_off(self):
return starpilot_state.car_state.hasAutoTune and self._params.get_bool("ForceAutoTuneOff")
def _activate_target(self, target_id: str | None):
if not target_id:
return
prefix, _, value = target_id.partition(":")
if prefix == "select":
self._controller._on_select(value)
def _forcing_torque_controller(self):
return not starpilot_state.car_state.isAngleCar and self._params.get_bool("ForceTorqueController")
def _draw_header(self, rect: rl.Rectangle):
draw_settings_panel_header(rect, tr("Steering"),
tr("Fine-tune lateral control, lane changes, and steering behavior."),
subtitle_size=22)
def _torque_tuning_active(self):
return starpilot_state.car_state.isTorqueCar or self._forcing_torque_controller() or self._using_nnff()
def _build_toggle_defs(self) -> list[dict]:
p = self._controller._params
cs = starpilot_state.car_state
toggles: list[dict] = []
def _manual_tuning_values_enabled(self):
if starpilot_state.car_state.hasAutoTune:
return self._forcing_auto_tune_off()
return not self._forcing_auto_tune()
toggles.append({
"key": "AdvancedLateralTune",
"title": tr("Advanced Lateral Tuning"),
"subtitle": tr("Advanced steering control changes to fine-tune how openpilot drives."),
"get": lambda: p.get_bool("AdvancedLateralTune"),
"set": lambda s: p.put_bool("AdvancedLateralTune", s),
})
def _build_view(self):
adv = self._advanced_enabled
torque = self._torque_tuning_active
manual = self._manual_tuning_values_enabled
nnff = self._using_nnff
toggles.append({
"key": "AlwaysOnLateral",
"title": tr("Always On Lateral"),
"subtitle": tr("Keep lateral control active even without openpilot engaged."),
"get": lambda: p.get_bool("AlwaysOnLateral"),
"set": lambda s: (_confirm_reboot_toggle(p, "AlwaysOnLateral", s)
if s else p.put_bool("AlwaysOnLateral", False)),
})
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),
]),
]
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,
metrics=COMPACT_PANEL_METRICS,
toggles.append({
"key": "LaneChanges",
"title": tr("Lane Changes"),
"subtitle": tr("Allow openpilot to change lanes."),
"get": lambda: p.get_bool("LaneChanges"),
"set": lambda s: p.put_bool("LaneChanges", s),
})
toggles.append({
"key": "PauseLateralOnSignal",
"title": tr("Turn Signal Only"),
"subtitle": tr("Only pause steering below the set speed when the turn signal is active."),
"get": lambda: p.get_bool("PauseLateralOnSignal"),
"set": lambda s: p.put_bool("PauseLateralOnSignal", s),
})
toggles.append({
"key": "NudgelessLaneChange",
"title": tr("Auto Lane Changes"),
"subtitle": tr("When the turn signal is on, openpilot will automatically change lanes without a nudge."),
"get": lambda: p.get_bool("NudgelessLaneChange"),
"set": lambda s: p.put_bool("NudgelessLaneChange", s),
})
toggles.append({
"key": "OneLaneChange",
"title": tr("One Per Signal"),
"subtitle": tr("Limit automatic lane changes to one per turn-signal activation."),
"get": lambda: p.get_bool("OneLaneChange"),
"set": lambda s: p.put_bool("OneLaneChange", s),
})
toggles.append({
"key": "TurnDesires",
"title": tr("Force Turn Desires"),
"subtitle": tr("While driving below the minimum lane change speed with an active turn signal, instruct openpilot to turn."),
"get": lambda: p.get_bool("TurnDesires"),
"set": lambda s: (p.put_bool("TurnDesires", s),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
})
toggles.append({
"key": "NavDesiresAllowed",
"title": tr("Use Route Desires"),
"subtitle": tr("Allow an active navigation route to request keep-left, keep-right, and low-speed turn desires."),
"get": lambda: p.get_bool("NavDesiresAllowed"),
"set": lambda s: p.put_bool("NavDesiresAllowed", s),
})
toggles.append({
"key": "NNFF",
"title": tr("NNFF"),
"subtitle": tr("Neural Network FeedForward controller — uses a trained model to predict steering torque."),
"get": lambda: p.get_bool("NNFF"),
"set": lambda s: (p.put_bool("NNFF", s),
s and p.put_bool("NNFFLite", False),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
"enabled": cs.hasNNFFLog and not cs.isAngleCar,
"disabled_label": tr("Not Available"),
})
toggles.append({
"key": "NNFFLite",
"title": tr("NNFF Lite"),
"subtitle": tr("Lightweight NNFF steering logic when the full model is off."),
"get": lambda: p.get_bool("NNFFLite"),
"set": lambda s: (p.put_bool("NNFFLite", s),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
"enabled": not cs.isAngleCar,
"disabled_label": tr("Not Available"),
})
toggles.append({
"key": "ForceAutoTune",
"title": tr("Force Auto-Tune On"),
"subtitle": tr("Force-enable live auto-tuning for friction and lateral acceleration."),
"get": lambda: p.get_bool("ForceAutoTune"),
"set": lambda s: (p.put_bool("ForceAutoTune", s),
s and p.put_bool("ForceAutoTuneOff", False),
_sync_parent(p, "AdvancedLateralTune", _ADVANCED_LATERAL_KEYS)),
"enabled": not cs.hasAutoTune and cs.isTorqueCar and not cs.isAngleCar,
"disabled_label": tr("Not Available"),
})
toggles.append({
"key": "ForceAutoTuneOff",
"title": tr("Force Auto-Tune Off"),
"subtitle": tr("Force-disable live auto-tuning and use your set values instead."),
"get": lambda: p.get_bool("ForceAutoTuneOff"),
"set": lambda s: (p.put_bool("ForceAutoTuneOff", s),
s and p.put_bool("ForceAutoTune", False),
_sync_parent(p, "AdvancedLateralTune", _ADVANCED_LATERAL_KEYS)),
"enabled": cs.hasAutoTune and cs.isTorqueCar and not cs.isAngleCar,
"disabled_label": tr("Not Available"),
})
toggles.append({
"key": "ForceTorqueController",
"title": tr("Force Torque Ctrl"),
"subtitle": tr("Use torque-based steering control instead of angle-based for smoother lane keeping, especially in curves."),
"get": lambda: p.get_bool("ForceTorqueController"),
"set": lambda s: (p.put_bool("ForceTorqueController", s),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
"enabled": not cs.isTorqueCar and not cs.isAngleCar,
"disabled_label": tr("Not Available"),
})
return toggles
def _rebuild_toggle_grid(self):
self._toggle_grid.clear()
for td in self._build_toggle_defs():
self._toggle_grid.add_tile(ToggleTile(
title=td["title"],
get_state=td["get"],
set_state=td["set"],
bg_color=PANEL_STYLE.accent,
desc=td.get("subtitle", ""),
is_enabled=td.get("enabled", True),
disabled_label=td.get("disabled_label", ""),
))
def _measure_content_height(self, width: float) -> float:
sections = self._build_left_sections()
left_h = self._stacked_section_height([s["height"] for s in sections if s.get("visible", True)])
tiles_height = 0.0
if self._toggle_grid.tiles:
N = len(self._toggle_grid.tiles)
gap = self._toggle_grid.gap
if self._uses_two_columns(width):
cols = 2
tile_rows = (N + cols - 1) // cols
tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0
tiles_content_h = tile_rows * 130 + tile_gaps
tiles_height = self._section_block_height(tiles_content_h + 24)
else:
cols = 3
tile_rows = (N + cols - 1) // cols
tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0
tiles_content_h = tile_rows * 130 + tile_gaps
tiles_height = SECTION_GAP + self._section_block_height(tiles_content_h + 24)
if self._uses_two_columns(width):
vh = self._scroll_rect.height if self._scroll_rect and self._scroll_rect.height > 0 else tiles_height
return max(left_h, vh)
return left_h + tiles_height
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
self._interactive_rects.clear()
y = rect.y + self._scroll_offset
self._draw_panel_content(y, rect.x, width)
def _draw_panel_content(self, y: float, x: float, width: float):
sections = self._build_left_sections()
drawn_first = False
if self._uses_two_columns(width):
column_w = self._column_width(width)
curr_y = y
for section in sections:
if not section.get("rows"):
continue
if drawn_first:
curr_y += SECTION_GAP
drawn_first = True
draw_section_header(rl.Rectangle(x, curr_y, column_w, SECTION_HEADER_HEIGHT),
section["title"], style=PANEL_STYLE)
curr_y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
row_count = len(section["rows"])
container_rect = rl.Rectangle(x, curr_y, column_w, row_count * ROW_HEIGHT)
draw_list_group_shell(container_rect, style=PANEL_STYLE)
for index, row in enumerate(section["rows"]):
row_rect = rl.Rectangle(x, curr_y + index * ROW_HEIGHT, column_w, ROW_HEIGHT)
self._draw_row(row_rect, row, is_last=index == row_count - 1)
curr_y += row_count * ROW_HEIGHT
if self._toggle_grid.tiles:
rx = x + column_w + self.COLUMN_GAP
draw_section_header(rl.Rectangle(rx, y, column_w, SECTION_HEADER_HEIGHT),
tr("Toggles"), style=PANEL_STYLE)
right_container_y = y + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
N = len(self._toggle_grid.tiles)
cols = 2
self._toggle_grid._columns = cols
gap = self._toggle_grid.gap
tile_rows = (N + cols - 1) // cols
tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0
tiles_content_h = tile_rows * 130 + tile_gaps
needed_height = tiles_content_h + 24
viewport_remaining = (self._scroll_rect.y + self._scroll_rect.height) - right_container_y
container_h = max(needed_height, viewport_remaining)
draw_list_group_shell(rl.Rectangle(rx, right_container_y, column_w, container_h), style=PANEL_STYLE)
self._toggle_grid.set_parent_rect(self._scroll_rect)
self._toggle_grid.render(rl.Rectangle(rx + 12, right_container_y + 12, column_w - 24, container_h - 24))
else:
curr_y = y
for section in sections:
if not section.get("rows"):
continue
if drawn_first:
curr_y += SECTION_GAP
drawn_first = True
draw_section_header(rl.Rectangle(x, curr_y, width, SECTION_HEADER_HEIGHT),
section["title"], style=PANEL_STYLE)
curr_y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
row_count = len(section["rows"])
container_rect = rl.Rectangle(x, curr_y, width, row_count * ROW_HEIGHT)
draw_list_group_shell(container_rect, style=PANEL_STYLE)
for index, row in enumerate(section["rows"]):
row_rect = rl.Rectangle(x, curr_y + index * ROW_HEIGHT, width, ROW_HEIGHT)
self._draw_row(row_rect, row, is_last=index == row_count - 1)
curr_y += row_count * ROW_HEIGHT
if self._toggle_grid.tiles:
curr_y += SECTION_GAP
draw_section_header(rl.Rectangle(x, curr_y, width, SECTION_HEADER_HEIGHT),
tr("Toggles"), style=PANEL_STYLE)
curr_y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
N = len(self._toggle_grid.tiles)
cols = 3
self._toggle_grid._columns = cols
gap = self._toggle_grid.gap
avail_w = width - 24
tile_rows = (N + cols - 1) // cols
tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0
tiles_content_h = tile_rows * 130 + tile_gaps
draw_list_group_shell(rl.Rectangle(x, curr_y, width, tiles_content_h + 24), style=PANEL_STYLE)
self._toggle_grid.set_parent_rect(self._scroll_rect)
self._toggle_grid.render(rl.Rectangle(x + 12, curr_y + 12, avail_w, tiles_content_h))
def _draw_row(self, rect: rl.Rectangle, row: dict, is_last: bool):
target_id = row["target_id"]
is_enabled = row.get("is_enabled", True)
hovered, pressed = self._interactive_state(target_id, rect) if is_enabled else (False, False)
draw_selection_list_row(
rect, title=row["title"], subtitle=row.get("subtitle", ""),
action_text=row["get_value"](), hovered=hovered and is_enabled,
pressed=pressed and is_enabled, is_last=is_last,
action_width=188, action_pill=True,
action_pill_width=row.get("pill_width", 108), action_pill_height=44,
title_size=34, subtitle_size=22, action_text_size=18,
row_separator=PANEL_STYLE.divider_color,
action_fill=PANEL_STYLE.current_fill if is_enabled else _with_alpha(PANEL_STYLE.current_fill, 120),
action_border=PANEL_STYLE.current_border if is_enabled else _with_alpha(PANEL_STYLE.current_border, 100),
action_text_color=AetherListColors.HEADER if is_enabled else AetherListColors.MUTED,
)
def _build_left_sections(self) -> list[dict]:
p = self._controller._params
cs = starpilot_state.car_state
sections: list[dict] = []
alt = p.get_bool("AdvancedLateralTune")
aol = p.get_bool("AlwaysOnLateral")
lc = p.get_bool("LaneChanges")
nlc = p.get_bool("NudgelessLaneChange")
pos = p.get_bool("PauseLateralOnSignal")
if alt:
rows: list[dict] = []
if cs.steerActuatorDelay != 0:
rows.append({
"target_id": "select:SteerDelay",
"title": tr("Actuator Delay"),
"subtitle": tr("The time between openpilot's steering command and the vehicle's response."),
"get_value": lambda: f"{p.get_float('SteerDelay'):.2f}s",
"pill_width": 120,
})
if cs.friction != 0:
rows.append({
"target_id": "select:SteerFriction",
"title": tr("Friction"),
"subtitle": tr("Compensates for steering friction around center."),
"get_value": lambda: f"{p.get_float('SteerFriction'):.3f}",
"pill_width": 120,
})
if cs.steerKp != 0:
rows.append({
"target_id": "select:SteerKP",
"title": tr("Kp Factor"),
"subtitle": tr("How strongly openpilot corrects lateral position."),
"get_value": lambda: f"{p.get_float('SteerKP'):.2f}",
"pill_width": 120,
})
if cs.latAccelFactor != 0:
rows.append({
"target_id": "select:SteerLatAccel",
"title": tr("Lateral Acceleration"),
"subtitle": tr("Maps steering torque to turning response."),
"get_value": lambda: f"{p.get_float('SteerLatAccel'):.2f}",
"pill_width": 120,
})
if cs.steerRatio != 0:
rows.append({
"target_id": "select:SteerRatio",
"title": tr("Steer Ratio"),
"subtitle": tr("Adjust the relationship between steering wheel input and road-wheel angle."),
"get_value": lambda: f"{p.get_float('SteerRatio'):.2f}",
"pill_width": 120,
})
sections.append({
"title": tr("Advanced Tuning"),
"rows": rows,
"visible": True,
"height": self._section_block_height(self._section_height(len(rows), ROW_HEIGHT)),
})
if aol:
aol_rows = [{
"target_id": "select:PauseAOLOnBrake",
"title": tr("Pause AOL On Brake"),
"subtitle": tr("Pause Always On Lateral below this speed while the brake is pressed."),
"get_value": lambda: f"{p.get_int('PauseAOLOnBrake')} mph",
"pill_width": 140,
}]
sections.append({
"title": tr("Always On Lateral"),
"rows": aol_rows,
"visible": True,
"height": self._section_block_height(self._section_height(len(aol_rows), ROW_HEIGHT)),
})
if lc:
lc_rows: list[dict] = [{
"target_id": "select:MinimumLaneChangeSpeed",
"title": tr("Min Lane Change Speed"),
"subtitle": tr("Lowest speed at which openpilot will change lanes."),
"get_value": lambda: f"{p.get_int('MinimumLaneChangeSpeed')} mph",
"pill_width": 140,
}]
if nlc:
lc_rows.append({
"target_id": "select:LaneChangeTime",
"title": tr("Lane Change Delay"),
"subtitle": tr("Delay before the start of an automatic lane change. 0 = Instant."),
"get_value": lambda: tr("Instant") if p.get_float("LaneChangeTime") == 0
else f"{p.get_float('LaneChangeTime'):.1f}s",
"pill_width": 120,
})
lc_rows.append({
"target_id": "select:LaneDetectionWidth",
"title": tr("Min Lane Width"),
"subtitle": tr("Prevent automatic lane changes into lanes narrower than this width."),
"get_value": lambda: f"{p.get_float('LaneDetectionWidth'):.1f} ft",
"pill_width": 120,
})
lc_rows.append({
"target_id": "select:LaneChangeSmoothing",
"title": tr("Lane Change Smoothing"),
"subtitle": tr("How smoothly openpilot commits to a lane change. 10 = Stock, 1 = Smoothest."),
"get_value": lambda: tr("Stock") if p.get_int("LaneChangeSmoothing") == 10
else f"{p.get_int('LaneChangeSmoothing')}",
"pill_width": 120,
})
sections.append({
"title": tr("Lane Changes"),
"rows": lc_rows,
"visible": True,
"height": self._section_block_height(self._section_height(len(lc_rows), ROW_HEIGHT)),
})
paus_rows: list[dict] = [{
"target_id": "select:PauseLateralSpeed",
"title": tr("Pause Lateral Below"),
"subtitle": tr("Pause steering below the set speed."),
"get_value": lambda: f"{p.get_int('PauseLateralSpeed')} mph",
"pill_width": 140,
}]
if pos:
paus_rows.append({
"target_id": "select:LateralResumeDelay",
"title": tr("Resume Delay"),
"subtitle": tr("Delay before lateral resumes after the turn signal is turned off. 0 = Off."),
"get_value": lambda: tr("Off") if p.get_float("LateralResumeDelay") == 0
else f"{p.get_float('LateralResumeDelay'):.1f}s",
"pill_width": 120,
})
sections.append({
"title": tr("Pause Lateral"),
"rows": paus_rows,
"visible": True,
"height": self._section_block_height(self._section_height(len(paus_rows), ROW_HEIGHT)),
})
return sections
# ═══════════════════════════════════════════════════════════════
# StarPilotLateralLayout — top-level hub with 3 tabs
# StarPilotLateralLayout — top-level hybrid panel
# ═══════════════════════════════════════════════════════════════
class StarPilotLateralLayout(_SettingsPage):
SLIDER_COLOR = "#5B9BD5"
def __init__(self):
super().__init__()
self._manager_view = SteeringManagerView(self)
self._sub_panels = {
"advanced_lateral": StarPilotAdvancedLateralLayout(),
}
self._wire_sub_panels()
self._build_view()
def _on_select(self, key: str):
if key == "PauseAOLOnBrake":
self._show_slider("PauseAOLOnBrake", 0, 100, unit=" mph")
elif key == "PauseLateralSpeed":
def on_speed_close(res, val):
if res == DialogResult.CONFIRM:
self._params.put_int("PauseLateralSpeed", int(val))
self._params.put_bool("QOLLateral", int(val) > 0)
current = self._params.get_int("PauseLateralSpeed")
gui_app.push_widget(AetherSliderDialog(
tr("Pause Lateral Below"), 0, 100, 1, current, on_speed_close,
unit=" mph", color=self.SLIDER_COLOR))
elif key == "LateralResumeDelay":
self._show_slider("LateralResumeDelay", 0.0, 5.0, step=0.1, unit="s", value_type="float")
elif key == "MinimumLaneChangeSpeed":
self._show_slider("MinimumLaneChangeSpeed", 0, 100, unit=" mph")
elif key == "LaneChangeTime":
self._show_slider("LaneChangeTime", 0.0, 5.0, step=0.1, unit="s", value_type="float")
elif key == "LaneDetectionWidth":
self._show_slider("LaneDetectionWidth", 0.0, 15.0, step=0.1, unit=" ft", value_type="float")
elif key == "LaneChangeSmoothing":
self._show_lane_smoothing()
elif key == "SteerDelay":
self._show_slider("SteerDelay", 0.01, 1.0, step=0.01, unit="s", value_type="float")
elif key == "SteerFriction":
self._show_slider("SteerFriction", 0.0, max(1.0, starpilot_state.car_state.friction * 1.5), step=0.01, value_type="float")
elif key == "SteerKP":
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")
elif key == "SteerLatAccel":
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")
elif key == "SteerRatio":
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")
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")},
]
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) if s else self._params.put_bool("AlwaysOnLateral", False)),
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")),
SettingRow("PauseLateralOnSignal", "toggle", tr_noop("Turn Signal Only"),
subtitle=tr_noop("Only pause steering when the turn signal is active."),
get_state=lambda: self._params.get_bool("PauseLateralOnSignal"),
set_state=lambda s: self._params.put_bool("PauseLateralOnSignal", s),
visible=lambda: self._params.get_bool("QOLLateral") and self._params.get_int("PauseLateralSpeed") > 0),
SettingRow("LateralResumeDelay", "value", tr_noop("Lateral Resume Delay"),
subtitle=tr_noop("Delay before steering resumes after the turn signal turns off. Set to 0 to disable."),
get_value=lambda: "Off" if self._params.get_float("LateralResumeDelay") == 0 else f"{self._params.get_float('LateralResumeDelay'):.1f}s",
on_click=lambda: self._show_slider("LateralResumeDelay", 0.0, 5.0, step=0.1, unit=" s", value_type="float"),
visible=lambda: self._params.get_bool("QOLLateral") and self._params.get_int("PauseLateralSpeed") > 0 and self._params.get_bool("PauseLateralOnSignal")),
], tab_key="steering"),
# ── 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"),
# ── 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"),
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("NavDesiresAllowed", "toggle", tr_noop("Use Route Desires"),
subtitle=tr_noop("Allow an active navigation route to request keep-left, keep-right, and low-speed turn desires."),
get_state=lambda: self._params.get_bool("NavDesiresAllowed"),
set_state=lambda s: self._params.put_bool("NavDesiresAllowed", 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"),
]
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,
metrics=COMPACT_PANEL_METRICS,
)
def _show_modal_pace_selector(self):
def _show_lane_smoothing(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.push_widget(AetherSliderDialog(tr("Lane Change Smoothing"), 1, 10, 1, current, on_close, color=PANEL_STYLE.accent))
gui_app.push_widget(AetherSliderDialog(tr("Lane Change Smoothing"), 1, 10, 1, current, on_close,
color=self.SLIDER_COLOR))