From 0b26d1c050892aac2b6253333496b6531a09c792 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:41:23 -0400 Subject: [PATCH] BigUI WIP: Start of Lateral design pass --- .../layouts/settings/starpilot/aethergrid.py | 2 + .../ui/layouts/settings/starpilot/lateral.py | 735 ++++++++---------- 2 files changed, 326 insertions(+), 411 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 5d28f0b2c..0d3099c28 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -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: diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index f0ea63f82..5e0ad039b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -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))