BigUI WIP: Start of Lateral design pass

This commit is contained in:
firestarsdog
2026-06-29 05:41:23 -04:00
parent 7d52c80bf4
commit 0b26d1c050
2 changed files with 326 additions and 411 deletions
@@ -2840,6 +2840,8 @@ class AetherSettingsView(PanelManagerView):
row = self._find_row(target_id)
if row is None:
return
if row.enabled is not None and not row.enabled():
return
if row.navigate_to:
self._controller._navigate_to(row.navigate_to)
elif row.on_click:
+324 -411
View File
@@ -12,17 +12,18 @@ 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 (
AetherListMetrics,
AetherListColors,
AetherSettingsView,
AetherSliderDialog,
BACK_BTN,
DEFAULT_PANEL_STYLE,
PanelManagerView,
HubTile,
SettingRow,
SettingSection,
TileGrid,
ToggleTile,
draw_list_group_shell,
draw_section_header,
draw_selection_list_row,
draw_settings_panel_header,
_with_alpha,
_draw_back_button,
_draw_rounded_fill,
_draw_rounded_stroke,
_point_hits,
)
@@ -51,9 +52,6 @@ CUSTOM_METRICS = AetherListMetrics(
utility_row_height=88,
)
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
@@ -61,11 +59,6 @@ _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):
@@ -75,431 +68,351 @@ def _sync_parent(params, parent_key, child_keys):
params.put_bool(parent_key, False)
class SteeringManagerView(PanelManagerView):
METRICS = CUSTOM_METRICS
class _BackButtonSettingsView(AetherSettingsView):
"""AetherSettingsView with a back button pill drawn in the scroll content header."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._back_btn_rect = None
self._has_header = False
def _target_at(self, mouse_pos):
if self._back_btn_rect and _point_hits(mouse_pos, self._back_btn_rect, None, pad_x=8, pad_y=8):
return BACK_BTN
return super()._target_at(mouse_pos)
def _activate_target(self, target_id):
if target_id == BACK_BTN:
self._controller._go_back()
else:
super()._activate_target(target_id)
def _draw_scroll_content(self, rect, width):
pill_h = 52
y = rect.y + self._scroll_offset
pill_rect = rl.Rectangle(rect.x, y, rect.width, pill_h)
center_y = pill_rect.y + pill_h / 2
_draw_rounded_fill(pill_rect, rl.Color(18, 16, 24, 200), radius_px=14)
_draw_rounded_stroke(pill_rect, rl.Color(255, 255, 255, 22), radius_px=14)
mouse_pos = gui_app.last_mouse_event.pos
is_pressed = getattr(self, '_pressed_target', None) == BACK_BTN
is_hovered = _point_hits(mouse_pos, pill_rect, None, pad_x=0, pad_y=0) and \
self._back_btn_rect and _point_hits(mouse_pos, self._back_btn_rect, None, pad_x=8, pad_y=8)
self._back_btn_rect = _draw_back_button(pill_rect, center_y, is_pressed, is_hovered)
crumb_x = pill_rect.x + 58
crumb_w = pill_rect.width - (crumb_x - pill_rect.x) - 20
crumb_rect = rl.Rectangle(crumb_x, pill_rect.y, crumb_w, pill_h)
self._breadcrumbs.draw(crumb_rect)
self._scroll_offset += pill_h + 12
super()._draw_scroll_content(rect, width)
class SteeringManagerView(AetherSettingsView):
"""Hub view with 3 category tiles leading to sub-panels."""
@property
def vertical_scrolling_disabled(self) -> bool:
return True
def __init__(self, controller: "StarPilotLateralLayout"):
super().__init__()
def __init__(self, controller, sections, **kwargs):
super().__init__(controller, sections, **kwargs)
self._controller = controller
self._shell_rect = rl.Rectangle(0, 0, 0, 0)
self._toggle_grid = TileGrid(columns=2, padding=12, force_square=True, min_tile_width=100, min_tile_height=130.0, max_tile_height=280.0)
self._toggle_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
self._child(self._toggle_grid)
self._hub_grid = TileGrid(columns=2, padding=12)
self._hub_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
self._child(self._hub_grid)
self._init_hub()
def show_event(self):
super().show_event()
starpilot_state.update(force=True)
self._rebuild_toggle_grid()
def _on_frame_created(self, frame) -> None:
self._shell_rect = frame.shell
def _init_hub(self):
hub_data = [
{
"key": "steering_behavior",
"title": tr("Steering Behavior"),
"desc": tr("Configure when steering engages, pauses, and resumes."),
"icon": "steering",
"color": "#8B5CF6",
},
{
"key": "lane_changes",
"title": tr("Lane Changes"),
"desc": tr("Configure lane change behavior, speed thresholds, and timing."),
"icon": "navigate",
"color": "#8B5CF6",
},
{
"key": "tuning",
"title": tr("Advanced Lateral Tuning"),
"desc": tr("Fine-tune steering response, feedforward, and auto-tuning."),
"icon": "system",
"color": "#8B5CF6",
},
]
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)
self._hub_grid.clear()
for d in hub_data:
self._hub_grid.add_tile(HubTile(
title=d["title"],
desc=d["desc"],
icon_key=d["icon"],
on_click=lambda k=d["key"]: self._controller._navigate_to(k),
bg_color=d["color"],
))
def _draw_header(self, rect: rl.Rectangle):
pass
def _build_toggle_defs(self) -> list[dict]:
p = self._controller._params
cs = starpilot_state.car_state
toggles: list[dict] = []
toggles.append({
"key": "AdvancedLateralTune",
"title": tr("Advanced Lateral Tuning"),
"subtitle": tr("Fine-tune steering response and auto-tuning."),
"get": lambda: p.get_bool("AdvancedLateralTune"),
"set": lambda s: p.put_bool("AdvancedLateralTune", s),
})
toggles.append({
"key": "AlwaysOnLateral",
"title": tr("Always On Lateral"),
"subtitle": tr("Steering stays active when ACC is off."),
"get": lambda: p.get_bool("AlwaysOnLateral"),
"set": lambda s: (_confirm_reboot_toggle(p, "AlwaysOnLateral", s)
if s else p.put_bool("AlwaysOnLateral", False)),
})
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 when 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("Signal triggers automatic lane change."),
"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("One lane change per 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("Follow turn intent below min lane change speed."),
"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 navigation to request lane keep and turns."),
"get": lambda: p.get_bool("NavDesiresAllowed"),
"set": lambda s: p.put_bool("NavDesiresAllowed", s),
})
toggles.append({
"key": "NNFF",
"title": tr("NNFF"),
"subtitle": tr("Neural net feedforward steering controller."),
"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 when 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 accel."),
"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 auto-tuning and use your set values."),
"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("Torque-based steering for smoother lane keeping."),
"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._page_grid = self._toggle_grid
defs = self._build_toggle_defs()
self._set_toggle_pages([defs[i:i+4] for i in range(0, len(defs), 4)])
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:
if not self._uses_two_columns(width):
self._toggle_grid._columns = 3
tiles_content_h = self._toggle_grid.measure_height(width - 24)
tiles_height = SECTION_GAP + self._section_block_height(tiles_content_h + 24)
if self._uses_two_columns(width):
column_w = self._column_width(width)
self._toggle_grid._columns = 2
grid_content_h = self.measure_page_grid_height(self._toggle_grid, column_w - 24) + 24
grid_container_h = grid_content_h + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
available_h = self._scroll_rect.height if self._scroll_rect else left_h
container_h = max(left_h, grid_container_h, available_h)
self._grid_container_h = container_h
return self._compute_two_column_height(container_h)
return left_h + tiles_height
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
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()
margin_x = 18.0
margin_y = 24.0
drawn_first = False
grid_x = rect.x + margin_x
grid_y = rect.y + margin_y
grid_w = rect.width - margin_x * 2
grid_h = rect.y + rect.height - grid_y - margin_y
if self._uses_two_columns(width):
column_w = self._column_width(width)
curr_y = y
self._scroll_rect = rl.Rectangle(grid_x, grid_y, grid_w, grid_h)
self._content_height = grid_h
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
self._draw_two_column_tile_grid(self._toggle_grid, rx, y, column_w, self._grid_container_h, title=tr("Toggles"), style=PANEL_STYLE)
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
self._toggle_grid._columns = 3
avail_w = width - 24
tiles_content_h = self._toggle_grid.measure_height(avail_w)
draw_list_group_shell(rl.Rectangle(x, curr_y, width, tiles_content_h + 24), style=PANEL_STYLE)
self._render_page_grid(self._toggle_grid, 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,
self._scroll_panel.set_enabled(self.is_visible)
self._scroll_offset = self._scroll_panel.update(
self._scroll_rect, self._scroll_rect.height
)
def _build_left_sections(self) -> list[dict]:
p = self._controller._params
cs = starpilot_state.car_state
sections: list[dict] = []
if self.vertical_scrolling_disabled:
self._scroll_offset = 0.0
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")
self._draw_scroll_content(self._scroll_rect, self._scroll_rect.width)
if alt:
rows: list[dict] = []
if cs.steerActuatorDelay != 0:
rows.append({
"target_id": "select:SteerDelay",
"title": tr("Actuator Delay"),
"subtitle": tr("Time between steering command and vehicle 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("Relationship between steering wheel 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 AOL below this speed while 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 lane changes into narrower lanes."),
"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("Smoothness of lane change commit. 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 signal 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
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
y = rect.y + self._scroll_offset
self._hub_grid.set_parent_rect(self._scroll_rect)
self._hub_grid.render(rl.Rectangle(rect.x, y, width, rect.height))
# ═══════════════════════════════════════════════════════════════
# StarPilotLateralLayout — top-level hybrid panel
# StarPilotLateralLayout — controller
# ═══════════════════════════════════════════════════════════════
class StarPilotLateralLayout(_SettingsPage):
def __init__(self):
super().__init__()
self._manager_view = SteeringManagerView(self)
self._build_sub_panels()
self._manager_view = SteeringManagerView(
self, [],
header_title=tr("Steering"),
header_subtitle=tr("Configure steering behavior, lane changes, and tuning parameters."),
panel_style=PANEL_STYLE,
)
def _build_sub_panels(self):
p = self._params
cs = starpilot_state.car_state
def alt_on():
return p.get_bool("AdvancedLateralTune")
def aol_on():
return p.get_bool("AlwaysOnLateral")
def lc_on():
return p.get_bool("LaneChanges")
def nlc_on():
return lc_on() and p.get_bool("NudgelessLaneChange")
def pos_on():
return p.get_bool("PauseLateralOnSignal")
def _show(key):
return lambda: self._on_select(key)
# ── Steering Behavior ──
self._steering_behavior_rows = [
SettingRow("AlwaysOnLateral", "toggle", tr("Always On Lateral"),
subtitle=tr("Steering stays active when ACC is off."),
get_state=lambda: p.get_bool("AlwaysOnLateral"),
set_state=lambda s: _confirm_reboot_toggle(p, "AlwaysOnLateral", s) if s else p.put_bool("AlwaysOnLateral", False)),
SettingRow("PauseAOLOnBrake", "value", tr("Pause AOL On Brake"),
subtitle=tr("Pause AOL below this speed while brake is pressed."),
get_value=lambda: f"{p.get_int('PauseAOLOnBrake')} mph",
on_click=_show("PauseAOLOnBrake"),
visible=aol_on),
SettingRow("PauseLateralOnSignal", "toggle", tr("Turn Signal Only"),
subtitle=tr("Only pause steering when turn signal is active."),
get_state=lambda: p.get_bool("PauseLateralOnSignal"),
set_state=lambda s: p.put_bool("PauseLateralOnSignal", s)),
SettingRow("PauseLateralSpeed", "value", tr("Pause Lateral Below"),
subtitle=tr("Pause steering below the set speed."),
get_value=lambda: f"{p.get_int('PauseLateralSpeed')} mph",
on_click=_show("PauseLateralSpeed")),
SettingRow("LateralResumeDelay", "value", tr("Resume Delay"),
subtitle=tr("Delay before lateral resumes after signal off. 0 = Off."),
get_value=lambda: tr("Off") if p.get_float("LateralResumeDelay") == 0 else f"{p.get_float('LateralResumeDelay'):.1f}s",
on_click=_show("LateralResumeDelay"),
visible=pos_on),
SettingRow("NavDesiresAllowed", "toggle", tr("Use Route Desires"),
subtitle=tr("Allow navigation to request lane keep and turns."),
get_state=lambda: p.get_bool("NavDesiresAllowed"),
set_state=lambda s: p.put_bool("NavDesiresAllowed", s)),
]
# ── Lane Changes ──
self._lane_change_rows = [
SettingRow("LaneChanges", "toggle", tr("Lane Changes"),
subtitle=tr("Allow openpilot to change lanes."),
get_state=lambda: p.get_bool("LaneChanges"),
set_state=lambda s: p.put_bool("LaneChanges", s)),
SettingRow("NudgelessLaneChange", "toggle", tr("Auto Lane Changes"),
subtitle=tr("Signal triggers automatic lane change."),
get_state=lambda: p.get_bool("NudgelessLaneChange"),
set_state=lambda s: p.put_bool("NudgelessLaneChange", s),
visible=lc_on),
SettingRow("OneLaneChange", "toggle", tr("One Per Signal"),
subtitle=tr("One lane change per signal activation."),
get_state=lambda: p.get_bool("OneLaneChange"),
set_state=lambda s: p.put_bool("OneLaneChange", s),
visible=nlc_on),
SettingRow("MinimumLaneChangeSpeed", "value", tr("Min Lane Change Speed"),
subtitle=tr("Lowest speed at which openpilot will change lanes."),
get_value=lambda: f"{p.get_int('MinimumLaneChangeSpeed')} mph",
on_click=_show("MinimumLaneChangeSpeed"),
visible=lc_on),
SettingRow("LaneChangeTime", "value", 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",
on_click=_show("LaneChangeTime"),
visible=nlc_on),
SettingRow("LaneDetectionWidth", "value", tr("Min Lane Width"),
subtitle=tr("Prevent lane changes into narrower lanes."),
get_value=lambda: f"{p.get_float('LaneDetectionWidth'):.1f} ft",
on_click=_show("LaneDetectionWidth"),
visible=nlc_on),
SettingRow("LaneChangeSmoothing", "value", tr("Lane Change Smoothing"),
subtitle=tr("Smoothness of lane change commit. 10 = Stock, 1 = Smoothest."),
get_value=lambda: tr("Stock") if p.get_int("LaneChangeSmoothing") == 10 else f"{p.get_int('LaneChangeSmoothing')}",
on_click=_show("LaneChangeSmoothing"),
visible=lc_on),
]
# ── Advanced Lateral Tuning ──
self._tuning_rows = [
SettingRow("AdvancedLateralTune", "toggle", tr("Advanced Lateral Tuning"),
subtitle=tr("Fine-tune steering response and auto-tuning."),
get_state=lambda: p.get_bool("AdvancedLateralTune"),
set_state=lambda s: p.put_bool("AdvancedLateralTune", s)),
SettingRow("NNFF", "toggle", tr("NNFF"),
subtitle=tr("Neural net feedforward steering controller."),
get_state=lambda: p.get_bool("NNFF"),
set_state=lambda s: (p.put_bool("NNFF", s),
s and p.put_bool("NNFFLite", False),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
enabled=lambda: cs.hasNNFFLog and not cs.isAngleCar,
disabled_label=tr("Not Available"),
visible=alt_on),
SettingRow("NNFFLite", "toggle", tr("NNFF Lite"),
subtitle=tr("Lightweight NNFF when full model is off."),
get_state=lambda: p.get_bool("NNFFLite"),
set_state=lambda s: (p.put_bool("NNFFLite", s),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
enabled=lambda: not cs.isAngleCar,
disabled_label=tr("Not Available"),
visible=alt_on),
SettingRow("ForceTorqueController", "toggle", tr("Force Torque Ctrl"),
subtitle=tr("Torque-based steering for smoother lane keeping."),
get_state=lambda: p.get_bool("ForceTorqueController"),
set_state=lambda s: (p.put_bool("ForceTorqueController", s),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
enabled=lambda: not cs.isTorqueCar and not cs.isAngleCar,
disabled_label=tr("Not Available"),
visible=alt_on),
SettingRow("TurnDesires", "toggle", tr("Force Turn Desires"),
subtitle=tr("Follow turn intent below min lane change speed."),
get_state=lambda: p.get_bool("TurnDesires"),
set_state=lambda s: (p.put_bool("TurnDesires", s),
_sync_parent(p, "LateralTune", _LATERAL_TUNE_KEYS)),
visible=alt_on),
SettingRow("ForceAutoTune", "toggle", tr("Force Auto-Tune On"),
subtitle=tr("Force-enable live auto-tuning for friction and lateral accel."),
get_state=lambda: p.get_bool("ForceAutoTune"),
set_state=lambda s: (p.put_bool("ForceAutoTune", s),
s and p.put_bool("ForceAutoTuneOff", False),
_sync_parent(p, "AdvancedLateralTune", _ADVANCED_LATERAL_KEYS)),
enabled=lambda: not cs.hasAutoTune and cs.isTorqueCar and not cs.isAngleCar,
disabled_label=tr("Not Available"),
visible=alt_on),
SettingRow("ForceAutoTuneOff", "toggle", tr("Force Auto-Tune Off"),
subtitle=tr("Force-disable auto-tuning and use your set values."),
get_state=lambda: p.get_bool("ForceAutoTuneOff"),
set_state=lambda s: (p.put_bool("ForceAutoTuneOff", s),
s and p.put_bool("ForceAutoTune", False),
_sync_parent(p, "AdvancedLateralTune", _ADVANCED_LATERAL_KEYS)),
enabled=lambda: cs.hasAutoTune and cs.isTorqueCar and not cs.isAngleCar,
disabled_label=tr("Not Available"),
visible=alt_on),
SettingRow("SteerDelay", "value", tr("Actuator Delay"),
subtitle=tr("Time between steering command and vehicle response."),
get_value=lambda: f"{p.get_float('SteerDelay'):.2f}s",
on_click=_show("SteerDelay"),
visible=lambda: alt_on() and cs.steerActuatorDelay != 0),
SettingRow("SteerFriction", "value", tr("Friction"),
subtitle=tr("Compensates for steering friction around center."),
get_value=lambda: f"{p.get_float('SteerFriction'):.3f}",
on_click=_show("SteerFriction"),
visible=lambda: alt_on() and cs.friction != 0),
SettingRow("SteerKP", "value", tr("Kp Factor"),
subtitle=tr("How strongly openpilot corrects lateral position."),
get_value=lambda: f"{p.get_float('SteerKP'):.2f}",
on_click=_show("SteerKP"),
visible=lambda: alt_on() and cs.steerKp != 0),
SettingRow("SteerLatAccel", "value", tr("Lateral Acceleration"),
subtitle=tr("Maps steering torque to turning response."),
get_value=lambda: f"{p.get_float('SteerLatAccel'):.2f}",
on_click=_show("SteerLatAccel"),
visible=lambda: alt_on() and cs.latAccelFactor != 0),
SettingRow("SteerRatio", "value", tr("Steer Ratio"),
subtitle=tr("Relationship between steering wheel and road-wheel angle."),
get_value=lambda: f"{p.get_float('SteerRatio'):.2f}",
on_click=_show("SteerRatio"),
visible=lambda: alt_on() and cs.steerRatio != 0),
]
# ── Build sub-panels ──
self._sub_panels["steering_behavior"] = _BackButtonSettingsView(
self,
[SettingSection(tr("Steering Behavior"), self._steering_behavior_rows, row_height=ROW_HEIGHT)],
header_title=tr("Steering Behavior"),
header_subtitle=tr("Configure when steering engages, pauses, and resumes."),
panel_style=PANEL_STYLE,
metrics=CUSTOM_METRICS,
)
self._sub_panels["lane_changes"] = _BackButtonSettingsView(
self,
[SettingSection(tr("Lane Changes"), self._lane_change_rows, row_height=ROW_HEIGHT)],
header_title=tr("Lane Changes"),
header_subtitle=tr("Configure lane change behavior, speed thresholds, and timing."),
panel_style=PANEL_STYLE,
metrics=CUSTOM_METRICS,
)
self._sub_panels["tuning"] = _BackButtonSettingsView(
self,
[SettingSection(tr("Advanced Lateral Tuning"), self._tuning_rows, row_height=ROW_HEIGHT)],
header_title=tr("Advanced Lateral Tuning"),
header_subtitle=tr("Fine-tune steering response, feedforward controllers, and auto-tuning."),
panel_style=PANEL_STYLE,
metrics=CUSTOM_METRICS,
)
self._wire_sub_panels()
def _on_select(self, key: str):
if key == "PauseAOLOnBrake":
@@ -540,4 +453,4 @@ class StarPilotLateralLayout(_SettingsPage):
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=self.SLIDER_COLOR))
color=self.SLIDER_COLOR))