Files
StarPilot/selfdrive/ui/layouts/settings/starpilot/longitudinal.py
T
2026-07-25 18:18:06 -04:00

1210 lines
64 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, replace
from collections.abc import Callable
import pyray as rl
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.system.ui.lib.application import FontWeight, MouseEvent, MousePos, gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import _SettingsPage
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AETHER_LIST_METRICS,
AetherListColors,
COMPACT_PANEL_METRICS,
AdjustorTogglesPanelView,
AetherAdjustorRow,
AetherSegmentedControl,
AetherSliderDialog,
DEFAULT_PANEL_STYLE,
PanelManagerView,
ParentToggle,
SettingRow,
SettingSection,
AetherSettingsView,
CardHubManagerView,
TileGrid,
HubTile,
TOGGLE_MIN_HEIGHT,
TOGGLE_ROW_HEIGHT,
RowToggleTile,
ToggleTile,
SPACING,
draw_section_header,
draw_list_group_shell,
SECTION_GAP,
SECTION_HEADER_HEIGHT,
SECTION_HEADER_GAP,
hex_to_color,
)
from openpilot.starpilot.common.accel_profile import (
ACCELERATION_PROFILES,
DECELERATION_PROFILES,
normalize_acceleration_profile,
normalize_deceleration_profile,
)
from openpilot.starpilot.common.experimental_state import sync_persist_experimental_state, sync_persist_chill_state
from openpilot.common.constants import CV
PANEL_STYLE = DEFAULT_PANEL_STYLE
ACCELERATION_PROFILE_OPTIONS = [
(ACCELERATION_PROFILES["STANDARD"], "Standard"),
(ACCELERATION_PROFILES["ECO"], "Eco"),
(ACCELERATION_PROFILES["SPORT"], "Sport"),
(ACCELERATION_PROFILES["SPORT_PLUS"], "Sport+"),
]
DECELERATION_PROFILE_OPTIONS = [
(DECELERATION_PROFILES["STANDARD"], "Standard"),
(DECELERATION_PROFILES["ECO"], "Eco"),
(DECELERATION_PROFILES["SPORT"], "Sport"),
]
SLC_FALLBACK_OPTIONS = [
(0, "Set Speed"),
(1, "Experimental Mode"),
(2, "Previous Limit"),
]
SLC_OVERRIDE_OPTIONS = [
(0, "None"),
(1, "Set With Gas Pedal"),
(2, "Max Set Speed"),
]
# ═══════════════════════════════════════════════════════════════
# AdaptiveSpeedView — nested panel with two adaptive speed tiles
# ═══════════════════════════════════════════════════════════════
class AdaptiveSpeedView(CardHubManagerView):
def __init__(self, controller):
super().__init__(controller, [], columns=2,
header_title=tr_noop("Adaptive Speed Controls"))
def _build_cards(self):
return [
{
"title": tr("Conditional Drive Mode"),
"desc": tr("Configure automated switching between Experimental and Chill Modes based on set conditions."),
"icon": "steering",
"on_click": lambda: self._controller._navigate_to("ce"),
},
{
"title": tr("Curve Speed Controller"),
"desc": tr("Configure speed control on curves and reset collected calibration data."),
"icon": "navigate",
"on_click": lambda: self._controller._navigate_to("csc"),
},
]
# ═══════════════════════════════════════════════════════════════
# LongitudinalManagerView — 6-card category hub
# ═══════════════════════════════════════════════════════════════
class LongitudinalManagerView(CardHubManagerView):
def __init__(self, controller, sections, **kwargs):
super().__init__(controller, sections, **kwargs)
def _build_cards(self):
return [
{
"title": tr("Longitudinal Tuning"),
"desc": tr("Configure acceleration profiles, lane changes, and route speed control."),
"icon": "steering",
"on_click": lambda: self._controller._navigate_to("tune"),
},
{
"title": tr("Advanced Actuators"),
"desc": tr("Adjust actuator delay, EV/Truck tuning, and launch/stop speeds/rates."),
"icon": "vehicle",
"on_click": lambda: self._controller._navigate_to("advanced"),
},
{
"title": tr("Speed Limit Controller"),
"desc": tr("Manage auto speed matching, confirmation, offsets, and source priority."),
"icon": "navigate",
"on_click": lambda: self._controller._navigate_to("slc"),
},
{
"title": tr("Adaptive Speed Controls"),
"desc": tr("Configure Curve Speed Controller and Conditional Experimental Mode triggers."),
"icon": "display",
"on_click": lambda: self._controller._navigate_to("adaptive_speed"),
},
{
"title": tr("Driving Personalities"),
"desc": tr("Customize follow distance and jerk/response metrics for each personality profile."),
"icon": "system",
"on_click": lambda: self._controller._navigate_to("personality"),
},
{
"title": tr("Quality of Life"),
"desc": tr("Configure cruise intervals, standstill behaviors, gear mapping, and weather presets."),
"icon": "sound",
"on_click": lambda: self._controller._navigate_to("daily"),
},
]
class ConditionalDriveModeView(AdjustorTogglesPanelView):
METRICS = COMPACT_PANEL_METRICS
TAB_HEIGHT = 98
TAB_BOTTOM_GAP = 26
def __init__(self, controller: StarPilotLongitudinalLayout):
super().__init__()
self._header_title = tr("Conditional Drive Mode")
self._controller = controller
self._init_segmented_control()
self._init_adjustors()
self._init_toggles()
def _init_segmented_control(self):
self._drive_mode_control = self._child(
AetherSegmentedControl(
[tr("OFF"), tr("Experimental"), tr("Chill")],
self._get_drive_mode_index,
self._on_drive_mode_change,
style=PANEL_STYLE,
suppress_background=True,
)
)
def _get_drive_mode_index(self):
if self._controller._params.get_bool("ConditionalExperimental"):
return 1
elif self._controller._params.get_bool("ConditionalChill"):
return 2
return 0
def _on_drive_mode_change(self, idx):
if idx == 0:
self._controller._params.put_bool("ConditionalExperimental", False)
self._controller._params.put_bool("ConditionalChill", False)
elif idx == 1:
self._controller._params.put_bool("ConditionalExperimental", True)
self._controller._params.put_bool("ConditionalChill", False)
elif idx == 2:
self._controller._params.put_bool("ConditionalExperimental", False)
self._controller._params.put_bool("ConditionalChill", True)
self._update_pagination()
def _init_toggles(self):
if PANEL_STYLE.toggle_row_mode:
self._toggle_grid = TileGrid(columns=1, padding=12, min_tile_height=TOGGLE_MIN_HEIGHT)
else:
self._toggle_grid = TileGrid(columns=2, padding=12, min_tile_height=130.0)
self._child(self._toggle_grid)
self.register_page_grid(self._toggle_grid)
cem_defs = [
{"title": tr("Curves"), "subtitle": tr("Switch to Experimental Mode on open-road curves."), "get_state": lambda: self._controller._params.get_bool("CECurves"), "set_state": lambda v: self._controller._params.put_bool("CECurves", v)},
{"title": tr("Curves w/ Lead"), "subtitle": tr("Switch on curves even when following a lead."), "get_state": lambda: self._controller._params.get_bool("CECurvesLead"), "set_state": lambda v: self._controller._params.put_bool("CECurvesLead", v), "is_enabled": lambda: self._controller._params.get_bool("CECurves"), "disabled_label": tr("Turn on Curves first")},
{"title": tr("Stop Lights/Signs"), "subtitle": tr("Switch when openpilot detects a stop."), "get_state": lambda: self._controller._params.get_bool("CEStopLights"), "set_state": lambda v: self._controller._params.put_bool("CEStopLights", v)},
{"title": tr("Lead Ahead"), "subtitle": tr("Switch when a slower/stopped vehicle is ahead."), "get_state": lambda: self._controller._params.get_bool("CELead"), "set_state": lambda v: self._controller._params.put_bool("CELead", v)},
{"title": tr("Slower Lead"), "subtitle": tr("Switch specifically for slower leads."), "get_state": lambda: self._controller._params.get_bool("CESlowerLead"), "set_state": lambda v: self._controller._params.put_bool("CESlowerLead", v), "is_enabled": lambda: self._controller._params.get_bool("CELead"), "disabled_label": tr("Turn on Lead first")},
{"title": tr("Stopped Lead"), "subtitle": tr("Switch specifically for stopped leads."), "get_state": lambda: self._controller._params.get_bool("CEStoppedLead"), "set_state": lambda v: self._controller._params.put_bool("CEStoppedLead", v), "is_enabled": lambda: self._controller._params.get_bool("CELead"), "disabled_label": tr("Turn on Lead first")},
{"title": tr("Signal Lane Detect"), "subtitle": tr("Don't trigger on turn signal if lines are clear."), "get_state": lambda: self._controller._params.get_bool("CESignalLaneDetection"), "set_state": lambda v: self._controller._params.put_bool("CESignalLaneDetection", v), "is_enabled": lambda: self._controller._params.get_int("CESignalSpeed") > 0, "disabled_label": tr("Needs Turn Signal speed > 0")},
{"title": tr("Status Widget"), "subtitle": tr("Show condition trigger on the drive screen."), "get_state": lambda: self._controller._params.get_bool("ShowCEMStatus"), "set_state": lambda v: self._controller._params.put_bool("ShowCEMStatus", v)},
{"title": tr("Persist Exp State"), "subtitle": tr("Keep manual Experimental override through reboots."), "get_state": lambda: self._controller._params.get_bool("PersistExperimentalState"), "set_state": self._controller._set_persist_experimental_state},
]
ccm_defs = [
{"title": tr("Stable Lead Ahead"), "subtitle": tr("Switch to Chill Mode when following a steady lead."), "get_state": lambda: self._controller._params.get_bool("CCMLead"), "set_state": lambda v: self._controller._params.put_bool("CCMLead", v)},
{"title": tr("Launch Assist"), "subtitle": tr("Temporarily switch to Chill from a stop."), "get_state": lambda: self._controller._params.get_bool("CCMLaunchAssist"), "set_state": lambda v: self._controller._params.put_bool("CCMLaunchAssist", v)},
{"title": tr("Status Widget"), "subtitle": tr("Show condition trigger on the drive screen."), "get_state": lambda: self._controller._params.get_bool("ShowCCMStatus"), "set_state": lambda v: self._controller._params.put_bool("ShowCCMStatus", v)},
{"title": tr("Persist Chill State"), "subtitle": tr("Keep manual Chill override through reboots."), "get_state": lambda: self._controller._params.get_bool("PersistChillState"), "set_state": self._controller._set_persist_chill_state},
]
self._cem_toggle_defs = cem_defs
self._ccm_toggle_defs = ccm_defs
self._update_pagination()
def _update_pagination(self):
mode = self._get_drive_mode_index()
page_size = self._compute_page_size(TOGGLE_ROW_HEIGHT)
if mode == 1:
pages = [self._cem_toggle_defs[i:i+page_size] for i in range(0, len(self._cem_toggle_defs), page_size)]
self._set_toggle_pages(pages)
elif mode == 2:
pages = [self._ccm_toggle_defs[i:i+page_size] for i in range(0, len(self._ccm_toggle_defs), page_size)]
self._set_toggle_pages(pages)
else:
self._set_toggle_pages([])
def _make_toggle_tile(self, info: dict) -> ToggleTile:
cls = RowToggleTile if PANEL_STYLE.toggle_row_mode else ToggleTile
kwargs = {
"title": info["title"],
"desc": info.get("subtitle", ""),
"get_state": info["get_state"],
"set_state": info["set_state"],
"bg_color": PANEL_STYLE.accent
}
if "is_enabled" in info:
kwargs["is_enabled"] = info["is_enabled"]
if "disabled_label" in info:
kwargs["disabled_label"] = info["disabled_label"]
return cls(**kwargs)
def _init_adjustors(self):
speed_unit = self._controller._speed_unit()
is_metric = self._controller._is_metric()
max_speed = 150.0 if is_metric else 100.0
specs = {
"CESpeed": {"title": tr("Below Speed"), "subtitle": "", "min": 0, "max": max_speed, "step": 1.0, "unit": speed_unit, "presets": [0, 20, 35, 55, 75], "labels": {}, "get": lambda: float(self._controller._params.get_int("CESpeed"))},
"CESpeedLead": {"title": tr("Speed w/ Lead"), "subtitle": "", "min": 0, "max": max_speed, "step": 1.0, "unit": speed_unit, "presets": [0, 20, 35, 55, 75], "labels": {}, "get": lambda: float(self._controller._params.get_int("CESpeedLead"))},
"CESignalSpeed": {"title": tr("Turn Signal Below"), "subtitle": "", "min": 0, "max": max_speed, "step": 1.0, "unit": speed_unit, "presets": [0, 20, 35, 55, 75], "labels": {0.0: tr("Off")}, "get": lambda: float(self._controller._params.get_int("CESignalSpeed"))},
"CEModelStopTime": {"title": tr("Predicted Stop In"), "subtitle": "", "min": 0, "max": 10.0, "step": 1.0, "unit": "s", "presets": [0, 3, 5, 7, 10], "labels": {0.0: tr("Off")}, "get": lambda: float(self._controller._params.get_int("CEModelStopTime"))},
"CCMSpeed": {"title": tr("Above Speed"), "subtitle": "", "min": 0, "max": max_speed, "step": 1.0, "unit": speed_unit, "presets": [0, 35, 55, 65, 80], "labels": {}, "get": lambda: float(self._controller._params.get_int("CCMSpeed"))},
"CCMSpeedLead": {"title": tr("Speed w/ Lead"), "subtitle": "", "min": 0, "max": max_speed, "step": 1.0, "unit": speed_unit, "presets": [0, 35, 55, 65, 80], "labels": {}, "get": lambda: float(self._controller._params.get_int("CCMSpeedLead"))},
"CCMSetSpeedMargin": {"title": tr("Set Speed Margin"), "subtitle": "", "min": 0, "max": 30.0 if is_metric else 15.0, "step": 1.0, "unit": speed_unit, "presets": [0, 5, 10, 15], "labels": {}, "get": lambda: float(self._controller._params.get_int("CCMSetSpeedMargin"))},
}
self._cem_keys = ["CESpeed", "CESpeedLead", "CESignalSpeed", "CEModelStopTime"]
self._ccm_keys = ["CCMSpeed", "CCMSpeedLead", "CCMSetSpeedMargin"]
for key, spec in specs.items():
adjustor = self._child(AetherAdjustorRow(
spec["title"], spec["subtitle"], spec["min"], spec["max"], spec["step"],
get_value=spec["get"],
on_change=lambda _v: None,
on_commit=None,
unit=spec["unit"],
labels=spec["labels"],
presets=spec["presets"],
is_active=lambda: False,
set_active=lambda active, k=key: self._show_slider(k) if active else None,
style=PANEL_STYLE, color=PANEL_STYLE.accent
))
adjustor.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
self._adjustor_rows[key] = adjustor
def _show_slider(self, key: str):
is_metric = self._controller._is_metric()
speed_unit = self._controller._speed_unit()
max_speed = 150.0 if is_metric else 100.0
specs = {
"CESpeed": {"title": tr("Below Speed"), "min": 0, "max": max_speed, "unit": speed_unit, "labels": {}, "presets": [0, 20, 35, 55, 75]},
"CESpeedLead": {"title": tr("Speed w/ Lead"), "min": 0, "max": max_speed, "unit": speed_unit, "labels": {}, "presets": [0, 20, 35, 55, 75]},
"CESignalSpeed": {"title": tr("Turn Signal Below"), "min": 0, "max": max_speed, "unit": speed_unit, "labels": {0.0: tr("Off")}, "presets": [0, 20, 35, 55, 75]},
"CEModelStopTime": {"title": tr("Predicted Stop In"), "min": 0, "max": 10.0, "unit": "s", "labels": {0.0: tr("Off")}, "presets": [0, 3, 5, 7, 10]},
"CCMSpeed": {"title": tr("Above Speed"), "min": 0, "max": max_speed, "unit": speed_unit, "labels": {}, "presets": [0, 35, 55, 65, 80]},
"CCMSpeedLead": {"title": tr("Speed w/ Lead"), "min": 0, "max": max_speed, "unit": speed_unit, "labels": {}, "presets": [0, 35, 55, 65, 80]},
"CCMSetSpeedMargin": {"title": tr("Set Speed Margin"), "min": 0, "max": 30.0 if is_metric else 15.0, "unit": speed_unit, "labels": {}, "presets": [0, 5, 10, 15]},
}
spec = specs[key]
original_val = float(self._controller._params.get_int(key))
def on_close(res, val):
if res == DialogResult.CONFIRM:
self._controller._params.put_int(key, int(val))
gui_app.push_widget(AetherSliderDialog(
title=spec["title"],
min_val=float(spec["min"]), max_val=float(spec["max"]), step=1.0,
current_val=original_val,
on_close=on_close, presets=[float(p) for p in spec["presets"]],
unit=spec["unit"], labels=spec["labels"], color=PANEL_STYLE.accent
))
def _draw_header(self, rect: rl.Rectangle):
pass
def _measure_content_height(self, content_width: float) -> float:
mode = self._get_drive_mode_index()
if mode == 0:
return self.TAB_HEIGHT + self.TAB_BOTTOM_GAP
keys = self._cem_keys if mode == 1 else self._ccm_keys
grid = self._toggle_grid
col_width = (content_width - SECTION_GAP) / 2 if self._uses_two_columns(content_width) else content_width
for key in keys:
self._adjustor_rows[key].custom_row_height = None
default_adjustor_h = float(AETHER_LIST_METRICS.adjustor_row_height)
left_h = len(keys) * default_adjustor_h + 16.0
num_tiles = 4 if self._has_pagination else len(grid.tiles)
if PANEL_STYLE.toggle_row_mode:
rows = num_tiles
tile_h = TOGGLE_ROW_HEIGHT
else:
rows = (num_tiles + 1) // 2 if self._uses_two_columns(content_width) else num_tiles
tile_h = grid.min_tile_height
pagination_space = 32.0 if self._has_pagination else 0.0
tiles_h = rows * tile_h + (rows - 1) * grid.gap + grid.gap * 2 + pagination_space
right_h = tiles_h
banner_overhead = 52.0
if self._uses_two_columns(content_width):
max_natural_h = max(left_h, right_h)
section_overhead = banner_overhead + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
if self._scroll_rect:
available_h = self._scroll_rect.height - section_overhead - self.TAB_HEIGHT - self.TAB_BOTTOM_GAP - 6.0
else:
available_h = max_natural_h
max_container_h = available_h
left_row_h = max(95.0, (max_container_h - 16.0) / max(1, len(keys)))
for key in keys:
self._adjustor_rows[key].custom_row_height = left_row_h
self._left_container_h = max_container_h
self._tiles_container_h = max_container_h
return self._compute_two_column_height(section_overhead + max_container_h) + self.TAB_HEIGHT + self.TAB_BOTTOM_GAP
else:
self._left_container_h = left_h
self._tiles_container_h = right_h
total = banner_overhead + left_h + SECTION_GAP + right_h + SECTION_HEADER_HEIGHT * 2 + SECTION_HEADER_GAP * 2
return total + self.TAB_HEIGHT + self.TAB_BOTTOM_GAP
def _draw_scroll_content(self, rect: rl.Rectangle, content_width: float):
y = rect.y + self._scroll_offset
header_w = content_width
bar_rect = rl.Rectangle(rect.x, y, header_w, self.TAB_HEIGHT)
draw_list_group_shell(bar_rect, style=PANEL_STYLE)
self._drive_mode_control.render(bar_rect)
y += self.TAB_HEIGHT + self.TAB_BOTTOM_GAP
mode = self._get_drive_mode_index()
if mode == 0:
return
banner_text = tr("Switch to Experimental Mode") if mode == 1 else tr("Switch to Chill Mode")
banner_rect = rl.Rectangle(rect.x, y, content_width, 40)
gui_label(banner_rect, banner_text, 40, AetherListColors.HEADER, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
y += 40 + 12
keys = self._cem_keys if mode == 1 else self._ccm_keys
grid = self._toggle_grid
col_width = (content_width - SECTION_GAP) / 2 if self._uses_two_columns(content_width) else content_width
draw_section_header(rl.Rectangle(rect.x, y, col_width, SECTION_HEADER_HEIGHT), tr("When"), style=PANEL_STYLE, title_size=38, align_center=True)
if self._uses_two_columns(content_width):
draw_section_header(rl.Rectangle(rect.x + col_width + SECTION_GAP, y, col_width, SECTION_HEADER_HEIGHT), tr("For"), style=PANEL_STYLE, title_size=38, align_center=True)
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
self._draw_adjustors(y, rect.x, col_width, keys)
tg_columns = 1 if PANEL_STYLE.toggle_row_mode else 2
if self._uses_two_columns(content_width):
self._draw_two_column_tile_grid(
grid, rect.x + col_width + SECTION_GAP, y, col_width,
self._tiles_container_h, title=None, style=PANEL_STYLE,
columns=tg_columns)
else:
y += self._left_container_h + SECTION_GAP
draw_section_header(rl.Rectangle(rect.x, y, col_width, SECTION_HEADER_HEIGHT), tr("For"), style=PANEL_STYLE, title_size=38, align_center=True)
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
self._draw_two_column_tile_grid(
grid, rect.x, y, col_width,
self._tiles_container_h, title=None, style=PANEL_STYLE,
columns=tg_columns)
def _draw_adjustors(self, y: float, x: float, width: float, keys: list[str]):
draw_list_group_shell(rl.Rectangle(x, y, width, self._left_container_h), style=PANEL_STYLE)
current_y = y + 4
for index, key in enumerate(keys):
adjustor = self._adjustor_rows[key]
row_h = adjustor.measure_height(width)
row_rect = rl.Rectangle(x, current_y, width, row_h)
adjustor.set_is_last(index == len(keys) - 1)
adjustor.set_parent_rect(self._scroll_rect)
adjustor.render(row_rect)
current_y += row_h
def _get_active_elements(self):
mode = self._get_drive_mode_index()
elems = [self._drive_mode_control]
if mode == 1:
elems += [self._adjustor_rows[k] for k in self._cem_keys]
elif mode == 2:
elems += [self._adjustor_rows[k] for k in self._ccm_keys]
elems.append(self._toggle_grid)
return elems
# ═══════════════════════════════════════════════════════════════
# StarPilotLongitudinalLayout — controller
# ═══════════════════════════════════════════════════════════════
class StarPilotLongitudinalLayout(_SettingsPage):
def __init__(self):
super().__init__()
self._keyboard = Keyboard(min_text_size=1)
# Track metric↔imperial transitions so we can rescale speed- and
# distance-typed params to the new unit when it flips.
self._last_is_metric: bool | None = None
self._build_view()
self._last_is_metric = self._is_metric()
def _longitudinal_enabled(self) -> bool:
return self._params.get_bool("LongitudinalTune")
def _advanced_enabled(self) -> bool:
return self._params.get_bool("AdvancedLongitudinalTune")
def _show_stop_tuning_values(self) -> bool:
return self._advanced_enabled()
def _make_parent(self, key: str, label: str, subtitle: str = "") -> ParentToggle:
return ParentToggle(
label=label,
subtitle=subtitle,
get_state=lambda k=key: self._params.get_bool(k),
set_state=lambda s, k=key: self._params.put_bool(k, s),
)
def _build_view(self):
ol = lambda: starpilot_state.car_state.hasOpenpilotLongitudinal
ce_on = lambda: self._params.get_bool("ConditionalExperimental")
cc_on = lambda: self._params.get_bool("ConditionalChill")
ce_lead = lambda: ce_on() and self._params.get_bool("CELead")
csc_on = lambda: self._params.get_bool("CurveSpeedController")
confirmation_on = lambda: self._params.get_bool("SLCConfirmation")
# ── 1. Longitudinal Tuning Rows ──
self._tune_rows = [
SettingRow("AccelProfile", "value", tr_noop("Acceleration Profile"),
subtitle=tr_noop("Choose how quickly openpilot speeds up."),
get_value=self._get_acceleration_profile_label,
on_click=self._show_acceleration_profile_selector,
visible=self._longitudinal_enabled),
SettingRow("DecelProfile", "value", tr_noop("Deceleration Profile"),
subtitle=tr_noop("Choose how firmly openpilot slows the car down."),
get_value=self._get_deceleration_profile_label,
on_click=self._show_deceleration_profile_selector,
visible=self._longitudinal_enabled),
SettingRow("HumanLaneChanges", "toggle", tr_noop("Human-Like Lane Changes"),
subtitle=tr_noop("Radar-informed behavior during lane changes."),
get_state=lambda: self._params.get_bool("HumanLaneChanges"),
set_state=lambda s: self._params.put_bool("HumanLaneChanges", s),
visible=lambda: self._longitudinal_enabled() and starpilot_state.car_state.hasRadar),
SettingRow("LeadDetection", "value", tr_noop("Lead Detection Sensitivity"),
subtitle=tr_noop("Control how aggressively openpilot detects and reacts to vehicles ahead."),
get_value=lambda: f"{self._params.get_int('LeadDetectionThreshold')}%",
on_click=lambda: self._show_slider("LeadDetectionThreshold", 25, 50, unit="%"),
visible=self._longitudinal_enabled),
SettingRow("NavLongitudinalAllowed", "toggle", tr_noop("Use Route Speed Control"),
subtitle=tr_noop("Allow an active navigation route to reduce cruise speed for upcoming turns, ramps, and roundabouts."),
get_state=lambda: self._params.get_bool("NavLongitudinalAllowed"),
set_state=lambda s: self._params.put_bool("NavLongitudinalAllowed", s),
visible=self._longitudinal_enabled),
]
# ── 2. Advanced Actuators Rows ──
adv = self._advanced_enabled
self._advanced_rows = [
SettingRow("EVTuning", "toggle", tr_noop("EV Tuning"),
subtitle=tr_noop("Acceleration tuning for EV and direct-drive vehicles."),
get_state=lambda: self._params.get_bool("EVTuning"),
set_state=self._set_ev_tuning,
visible=adv,
enabled=lambda: not self._params.get_bool("TruckTuning"),
disabled_label=tr_noop("Truck Active")),
SettingRow("TruckTuning", "toggle", tr_noop("Truck Tuning"),
subtitle=tr_noop("Stronger launch and acceleration for heavier vehicles."),
get_state=lambda: self._params.get_bool("TruckTuning"),
set_state=self._set_truck_tuning,
visible=adv,
enabled=lambda: not self._params.get_bool("EVTuning"),
disabled_label=tr_noop("EV Active")),
SettingRow("TrailerLoad", "value", tr_noop("Trailer Load"),
subtitle=tr_noop("Loaded trailer weight for tow-aware gas, brake, and conservative lateral assist."),
get_value=lambda: f"{self._params.get_int('TrailerLoad')} lb",
on_click=lambda: self._show_slider("TrailerLoad", 0, 15000, step=500, unit=" lb"),
visible=adv),
SettingRow("ActuatorDelay", "value", tr_noop("Actuator Delay"),
subtitle=tr_noop("Time between command and the vehicle's response."),
get_value=lambda: f"{self._params.get_float('LongitudinalActuatorDelay'):.2f}s",
on_click=lambda: self._show_slider("LongitudinalActuatorDelay", 0.0, 1.0, step=0.01, unit="s", value_type="float"),
visible=adv),
SettingRow("MaxAccel", "value", tr_noop("Maximum Acceleration"),
subtitle=tr_noop("Strongest acceleration openpilot is allowed to command."),
get_value=lambda: f"{self._params.get_float('MaxDesiredAcceleration'):.1f}m/s" if self._params.get_float("MaxDesiredAcceleration") is not None else "N/A",
on_click=lambda: self._show_slider("MaxDesiredAcceleration", 0.1, 4.0, step=0.1, unit="m/s", value_type="float"),
visible=adv),
SettingRow("StartAccel", "value", tr_noop("Start Acceleration"),
subtitle=tr_noop("Extra acceleration when moving away from a stop."),
get_value=lambda: f"{self._params.get_float('StartAccel'):.2f}m/s",
on_click=lambda: self._show_slider("StartAccel", 0.0, 4.0, step=0.01, unit="m/s", value_type="float"),
visible=adv),
SettingRow("StopAccel", "value", tr_noop("Stop Acceleration"),
subtitle=tr_noop("Brake force to hold the vehicle at a complete stop."),
get_value=lambda: f"{self._params.get_float('StopAccel'):.2f}m/s",
on_click=lambda: self._show_slider("StopAccel", -4.0, 0.0, step=0.01, unit="m/s", value_type="float"),
visible=adv),
SettingRow("StoppingRate", "value", tr_noop("Stopping Rate"),
subtitle=tr_noop("How quickly braking ramps up to bring the car to a stop."),
get_value=lambda: f"{self._params.get_float('StoppingDecelRate'):.3f}m/s",
on_click=lambda: self._show_slider("StoppingDecelRate", 0.001, 1.0, step=0.001, unit="m/s", value_type="float"),
visible=self._show_stop_tuning_values),
SettingRow("StartSpeed", "value", tr_noop("Start Speed"),
subtitle=tr_noop("Speed where openpilot exits the stopped state."),
get_value=lambda: f"{self._params.get_float('VEgoStarting'):.2f}m/s",
on_click=lambda: self._show_slider("VEgoStarting", 0.01, 1.0, step=0.01, unit="m/s", value_type="float"),
visible=self._show_stop_tuning_values),
SettingRow("StopSpeed", "value", tr_noop("Stop Speed"),
subtitle=tr_noop("Speed where openpilot considers the vehicle fully stopped."),
get_value=lambda: f"{self._params.get_float('VEgoStopping'):.2f}m/s",
on_click=lambda: self._show_slider("VEgoStopping", 0.01, 1.0, step=0.01, unit="m/s", value_type="float"),
visible=self._show_stop_tuning_values),
]
# ── 3. Speed Limit Controller (SLC) Rows ──
self._slc_rows = [
SettingRow("SLCFallback", "value", tr_noop("Fallback Speed"),
subtitle="",
get_value=lambda: self._profile_label_for_value(self._params.get_int("SLCFallback"), SLC_FALLBACK_OPTIONS),
on_click=lambda: self._show_labeled_select("Fallback Speed", "SLCFallback", SLC_FALLBACK_OPTIONS,
self._params.get_int("SLCFallback"))),
SettingRow("SLCOverride", "value", tr_noop("Override Speed"),
subtitle="",
get_value=lambda: self._profile_label_for_value(self._params.get_int("SLCOverride"), SLC_OVERRIDE_OPTIONS),
on_click=lambda: self._show_labeled_select("Override Speed", "SLCOverride", SLC_OVERRIDE_OPTIONS,
self._params.get_int("SLCOverride"))),
SettingRow("SLCPriority", "value", tr_noop("Source Priority"),
subtitle="",
get_value=self._get_priority_value,
on_click=self._on_priority_clicked),
SettingRow("SetSpeedLimit", "toggle", tr_noop("Auto Match Speed Limits"),
subtitle="",
get_state=lambda: self._params.get_bool("SetSpeedLimit"),
set_state=lambda s: self._params.put_bool("SetSpeedLimit", s)),
SettingRow("SLCConfirmation", "toggle", tr_noop("Confirm New Limits"),
subtitle="",
get_state=lambda: self._params.get_bool("SLCConfirmation"),
set_state=lambda s: self._params.put_bool("SLCConfirmation", s)),
SettingRow("SLCConfirmationLower", "toggle", tr_noop("Confirm Lower"),
subtitle="",
get_state=lambda: self._params.get_bool("SLCConfirmationLower"),
set_state=lambda s: self._params.put_bool("SLCConfirmationLower", s),
visible=confirmation_on),
SettingRow("SLCConfirmationHigher", "toggle", tr_noop("Confirm Higher"),
subtitle="",
get_state=lambda: self._params.get_bool("SLCConfirmationHigher"),
set_state=lambda s: self._params.put_bool("SLCConfirmationHigher", s),
visible=confirmation_on),
SettingRow("SLCLookHigher", "value", tr_noop("Higher Lookahead"),
subtitle="",
get_value=lambda: f"{self._params.get_int('SLCLookaheadHigher')}s",
on_click=lambda: self._show_slider("SLCLookaheadHigher", 0, 30, unit="s")),
SettingRow("SLCLookLower", "value", tr_noop("Lower Lookahead"),
subtitle="",
get_value=lambda: f"{self._params.get_int('SLCLookaheadLower')}s",
on_click=lambda: self._show_slider("SLCLookaheadLower", 0, 30, unit="s")),
SettingRow("SLCMapboxFiller", "toggle", tr_noop("Mapbox Fallback"),
subtitle="",
get_state=lambda: self._params.get_bool("SLCMapboxFiller"),
set_state=lambda s: self._params.put_bool("SLCMapboxFiller", s),
visible=self._mapbox_available),
SettingRow("VisionSpeedLimit", "toggle", tr_noop("Vision Detection"),
subtitle=tr_noop("Use the road camera to detect speed limit signs for SLC."),
get_state=lambda: self._params.get_bool("VisionSpeedLimitDetection"),
set_state=lambda s: self._params.put_bool("VisionSpeedLimitDetection", s)),
SettingRow("ShowSLCOffset", "toggle", tr_noop("Show SLC Offset"),
subtitle="",
get_state=lambda: self._params.get_bool("ShowSLCOffset"),
set_state=lambda s: self._params.put_bool("ShowSLCOffset", s)),
SettingRow("SpeedLimitSources", "toggle", tr_noop("Show Sources"),
subtitle="",
get_state=lambda: self._params.get_bool("SpeedLimitSources"),
set_state=lambda s: self._params.put_bool("SpeedLimitSources", s)),
SettingRow("SLCAbbreviatedSources", "toggle", tr_noop("Abbreviated Sources"),
subtitle=tr_noop("Render speed-limit sources as compact text labels (e.g. Dash-45)."),
get_state=lambda: self._params.get_bool("SLCAbbreviatedSources"),
set_state=lambda s: self._params.put_bool("SLCAbbreviatedSources", s),
visible=self._sources_visible),
SettingRow("SLCActiveSourcesOnly", "toggle", tr_noop("Active Sources Only"),
subtitle=tr_noop("Hide source rows that have no current speed limit reading."),
get_state=lambda: self._params.get_bool("SLCActiveSourcesOnly"),
set_state=lambda s: self._params.put_bool("SLCActiveSourcesOnly", s),
visible=self._sources_visible),
SettingRow("ConfigureOffsets", "value", tr_noop("SLC Offsets"),
subtitle=tr_noop("Per-limit speed adjustments for the Speed Limit Controller."),
get_value=lambda: tr_noop("Configure"),
on_click=self._show_slc_offsets_category),
]
# Initialize SLC Offsets rows
self._slc_offset_rows = []
for i in range(1, 8):
key = f"Offset{i}"
self._slc_offset_rows.append(SettingRow(
f"Offset{i}", "value", tr_noop(f"Offset {i}"),
subtitle="",
get_value=lambda k=key: f"{self._params.get_int(k)}{self._speed_unit()}",
on_click=lambda k=key: self._show_slider(k, *self._speed_range(), unit=self._speed_unit()),
))
# ── 4. Adaptive Speed Controls Rows (CES + CSC + CCM) ──
self._curve_speed_controller_rows = [
SettingRow("CalibratedLatAccel", "value", tr_noop("Calibrated Lateral Accel"),
subtitle=tr_noop("The learned lateral acceleration from collected driving data. Higher values allow faster cornering."),
get_value=lambda: f"{self._params_memory.get_float('CalibratedLateralAcceleration'):.2f} m/s",
on_click=None,
visible=csc_on),
SettingRow("CalibrationProgress", "value", tr_noop("Calibration Progress"),
subtitle=tr_noop("How much curve data has been collected. Normal for the value to stay low."),
get_value=lambda: f"{self._params_memory.get_float('CalibrationProgress'):.2f}%",
on_click=None,
visible=csc_on),
SettingRow("ResetCurve", "action", tr_noop("Reset Curve Data"),
subtitle=tr_noop("Reset collected user data for Curve Speed Controller."),
action_text=tr_noop("Reset"),
action_danger=True,
on_click=self._reset_curve_data,
visible=csc_on),
]
# ── 5. Driving Personalities Rows ──
self._personality_rows = [
SettingRow("Traffic", "value", tr_noop("Traffic"),
subtitle=tr_noop("Configure follow distance, smoothness, and response for traffic conditions."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_personality_profile_category("Traffic")),
SettingRow("Aggressive", "value", tr_noop("Aggressive"),
subtitle=tr_noop("Configure follow distance, smoothness, and response for aggressive driving."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_personality_profile_category("Aggressive")),
SettingRow("Standard", "value", tr_noop("Standard"),
subtitle=tr_noop("Configure follow distance, smoothness, and response for everyday driving."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_personality_profile_category("Standard")),
SettingRow("Relaxed", "value", tr_noop("Relaxed"),
subtitle=tr_noop("Configure follow distance, smoothness, and response for relaxed driving."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_personality_profile_category("Relaxed")),
]
# ── 6. Daily QOL & Weather Rows ──
self._daily_rows = [
SettingRow("CustomCruise", "value", tr_noop("Cruise Interval"),
subtitle="",
get_value=lambda: f"{max(1, self._params.get_int('CustomCruise'))}{self._speed_unit()}",
on_click=lambda: self._show_slider("CustomCruise", 1, 150 if self._is_metric() else 99,
unit=self._speed_unit(),
current_value=max(1, self._params.get_int("CustomCruise"))),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("CustomCruiseLong", "value", tr_noop("Cruise Long"),
subtitle="",
get_value=lambda: f"{max(1, self._params.get_int('CustomCruiseLong'))}{self._speed_unit()}",
on_click=lambda: self._show_slider("CustomCruiseLong", 1, 150 if self._is_metric() else 99,
unit=self._speed_unit(),
current_value=max(1, self._params.get_int("CustomCruiseLong"))),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("ForceStops", "toggle", tr_noop("Force Stops"),
subtitle="",
get_state=lambda: self._params.get_bool("ForceStops"),
set_state=lambda s: self._params.put_bool("ForceStops", s),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("ForceStopDist", "value", tr_noop("Force Stop Offset"),
subtitle="",
get_value=lambda: f"{self._params.get_int('ForceStopDistanceOffset'):+d} ft",
on_click=lambda: self._show_slider("ForceStopDistanceOffset", -20, 20, unit=" ft"),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("ForceStops")),
SettingRow("RadarTakeoffs", "toggle", tr_noop("Radar for Takeoffs"),
subtitle=tr_noop("Turns on/off using radar data to track leads at standstill, making following/takeoffs more responsive once leads move."),
get_state=lambda: self._params.get_bool("RadarTakeoffs"),
set_state=lambda s: self._params.put_bool("RadarTakeoffs", s),
visible=lambda: self._params.get_bool("QOLLongitudinal") and starpilot_state.car_state.hasRadar),
SettingRow("ForceStandstill", "toggle", tr_noop("Force Standstill"),
subtitle="",
get_state=lambda: self._params.get_bool("ForceStandstill"),
set_state=lambda s: self._params.put_bool("ForceStandstill", s),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("IncStoppedDist", "value", tr_noop("Stopped Distance"),
subtitle="",
get_value=lambda: f"{self._params.get_int('IncreasedStoppedDistance')}{self._distance_unit()}",
on_click=lambda: self._show_slider("IncreasedStoppedDistance", *self._distance_range(),
unit=self._distance_unit()),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("SetSpeedOffset", "value", tr_noop("Set Speed Offset"),
subtitle="",
get_value=lambda: f"+{self._params.get_int('SetSpeedOffset')}{self._speed_unit()}",
on_click=lambda: self._show_slider("SetSpeedOffset", 0, 150 if self._is_metric() else 99,
unit=self._speed_unit()),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("MapGears", "toggle", tr_noop("Map Gears"),
subtitle="",
get_state=lambda: self._params.get_bool("MapGears"),
set_state=lambda s: self._params.put_bool("MapGears", s),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("MapAccel", "toggle", tr_noop("Map Acceleration"),
subtitle="",
get_state=lambda: self._params.get_bool("MapAcceleration"),
set_state=lambda s: self._params.put_bool("MapAcceleration", s),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("MapGears")),
SettingRow("MapDecel", "toggle", tr_noop("Map Deceleration"),
subtitle="",
get_state=lambda: self._params.get_bool("MapDeceleration"),
set_state=lambda s: self._params.put_bool("MapDeceleration", s),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("MapGears")),
SettingRow("WeatherPresets", "toggle", tr_noop("Weather Condition Offsets"),
subtitle=tr_noop("Automatically adjust driving behavior based on real-time weather."),
get_state=lambda: self._params.get_bool("WeatherPresets"),
set_state=lambda s: self._params.put_bool("WeatherPresets", s),
visible=lambda: self._params.get_bool("QOLLongitudinal")),
SettingRow("LowVisibility", "value", tr_noop("Low Visibility"),
subtitle=tr_noop("Adjust parameters for fog, mist, and poor visibility conditions."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_weather_offsets_category("LowVisibility", tr_noop("Low Visibility")),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("WeatherPresets")),
SettingRow("Rain", "value", tr_noop("Rain"),
subtitle=tr_noop("Adjust parameters for light to moderate rain."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_weather_offsets_category("Rain", tr_noop("Rain")),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("WeatherPresets")),
SettingRow("RainStorm", "value", tr_noop("Rainstorms"),
subtitle=tr_noop("Adjust parameters for heavy rain and storms."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_weather_offsets_category("RainStorm", tr_noop("Rainstorms")),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("WeatherPresets")),
SettingRow("Snow", "value", tr_noop("Snow"),
subtitle=tr_noop("Adjust parameters for snowy and icy conditions."),
get_value=lambda: tr_noop("Configure"),
on_click=lambda: self._show_weather_offsets_category("Snow", tr_noop("Snow")),
visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("WeatherPresets")),
SettingRow("WeatherKey", "action", tr_noop("Set Weather Key"),
subtitle=tr_noop("Enter or remove your weather data API key."),
action_text=tr_noop("Set Key"),
on_click=self._set_weather_key,
visible=lambda: self._params.get_bool("QOLLongitudinal")),
]
self._manager_view = LongitudinalManagerView(
self, [],
header_title=tr_noop("Gas/Brake"),
header_subtitle=tr_noop("Fine-tune acceleration, braking, and driving behavior."),
panel_style=PANEL_STYLE,
)
pt_tune = self._make_parent("LongitudinalTune", "Longitudinal Tuning",
"Acceleration and braking control changes to fine-tune how openpilot drives.")
pt_advanced = self._make_parent("AdvancedLongitudinalTune", "Advanced Longitudinal Tuning",
"Advanced acceleration and braking changes for refining launch, stopping, and actuator response.")
pt_personality = self._make_parent("CustomPersonalities", "Driving Personalities")
pt_daily = self._make_parent("QOLLongitudinal", "Quality of Life")
pt_slc = self._make_parent("SpeedLimitController", "Speed Limit Controller",
"Limit the car's maximum speed to the current speed limit.")
pt_csc = self._make_parent("CurveSpeedController", "Curve Speed Controller",
"Configure speed control on curves and reset collected calibration data.")
self._sub_panels["ce"] = ConditionalDriveModeView(self)
self._sub_panels["csc"] = AetherSettingsView(
self,
[SettingSection(tr("Curve Speed Controller"), self._curve_speed_controller_rows)],
header_title=tr("Curve Speed Controller"),
header_subtitle=tr("Configure speed control on curves and reset collected calibration data."),
parent_toggle=pt_csc,
panel_style=PANEL_STYLE,
)
self._sub_panels["adaptive_speed"] = AdaptiveSpeedView(self)
# Register subpanels for Level 2 slide transitions
self._sub_panels["tune"] = AetherSettingsView(
self,
[SettingSection(title="", rows=self._tune_rows)],
header_title=tr_noop("Longitudinal Tuning"),
header_subtitle=tr_noop("Configure acceleration profiles, lane changes, and route speed control."),
parent_toggle=pt_tune,
panel_style=PANEL_STYLE,
)
self._sub_panels["advanced"] = AetherSettingsView(
self,
[SettingSection(title="", rows=self._advanced_rows)],
header_title=tr_noop("Advanced Actuators"),
header_subtitle=tr_noop("Adjust actuator delay, EV/Truck tuning, and launch/stop speeds/rates."),
parent_toggle=pt_advanced,
panel_style=PANEL_STYLE,
)
self._sub_panels["slc"] = AetherSettingsView(
self,
[SettingSection(title="", rows=self._slc_rows)],
header_title=tr_noop("Speed Limit Controller"),
header_subtitle=tr_noop("Manage auto speed matching, confirmation, offsets, and source priority."),
parent_toggle=pt_slc,
panel_style=PANEL_STYLE,
)
self._sub_panels["personality"] = AetherSettingsView(
self,
[SettingSection(title="", rows=self._personality_rows)],
header_title=tr_noop("Driving Personalities"),
header_subtitle=tr_noop("Customize follow distance and jerk/response metrics for each personality profile."),
parent_toggle=pt_personality,
panel_style=PANEL_STYLE,
)
self._sub_panels["daily"] = AetherSettingsView(
self,
[SettingSection(title="", rows=self._daily_rows)],
header_title=tr_noop("Daily QOL & Weather"),
header_subtitle=tr_noop("Configure cruise intervals, standstill behaviors, gear mapping, and weather presets."),
parent_toggle=pt_daily,
panel_style=PANEL_STYLE,
)
self._wire_sub_panels()
def _get_priority_value(self) -> str:
primary = self._params.get("SLCPriority1", encoding="utf-8") or "Map Data"
secondary = self._params.get("SLCPriority2", encoding="utf-8") or "None"
if primary in ("Highest", "Lowest") or secondary in ("", "None", primary):
return primary
return f"{primary}, {secondary}"
def _on_priority_clicked(self):
primary_options = ["Dashboard", "Map Data", "Vision", "Highest", "Lowest"]
if not starpilot_state.car_state.hasDashSpeedLimits:
primary_options.remove("Dashboard")
current_primary = self._params.get("SLCPriority1", encoding="utf-8") or "Map Data"
if current_primary not in primary_options:
current_primary = primary_options[0]
current_secondary = self._params.get("SLCPriority2", encoding="utf-8") or "None"
def on_secondary_select(primary, dialog, res):
if res == DialogResult.CONFIRM and dialog.selection:
self._params.put("SLCPriority1", primary)
self._params.put("SLCPriority2", dialog.selection)
def show_secondary_dialog(primary):
secondary_options = ["None"] + [option for option in ("Dashboard", "Map Data", "Vision") if option != primary]
if not starpilot_state.car_state.hasDashSpeedLimits and "Dashboard" in secondary_options:
secondary_options.remove("Dashboard")
selected_secondary = current_secondary if current_secondary in secondary_options else "None"
secondary_dialog = MultiOptionDialog(tr("SLC Secondary Priority"), secondary_options, selected_secondary,
callback=lambda res: on_secondary_select(primary, secondary_dialog, res))
gui_app.push_widget(secondary_dialog)
def on_primary_select(res):
if res != DialogResult.CONFIRM or not primary_dialog.selection:
return
if primary_dialog.selection in ("Highest", "Lowest"):
self._params.put("SLCPriority1", primary_dialog.selection)
self._params.put("SLCPriority2", "None")
return
show_secondary_dialog(primary_dialog.selection)
primary_dialog = MultiOptionDialog(tr("SLC Primary Priority"), primary_options, current_primary, callback=on_primary_select)
gui_app.push_widget(primary_dialog)
def _get_acceleration_profile_label(self) -> str:
value = normalize_acceleration_profile(self._params.get("AccelerationProfile", encoding="utf-8"))
return self._profile_label_for_value(value, ACCELERATION_PROFILE_OPTIONS)
def _get_deceleration_profile_label(self) -> str:
value = normalize_deceleration_profile(self._params.get("DecelerationProfile", encoding="utf-8"))
return self._profile_label_for_value(value, DECELERATION_PROFILE_OPTIONS)
def _show_acceleration_profile_selector(self):
self._show_labeled_select("Acceleration Profile", "AccelerationProfile", ACCELERATION_PROFILE_OPTIONS,
normalize_acceleration_profile(self._params.get("AccelerationProfile", encoding="utf-8")))
def _show_deceleration_profile_selector(self):
self._show_labeled_select("Deceleration Profile", "DecelerationProfile", DECELERATION_PROFILE_OPTIONS,
normalize_deceleration_profile(self._params.get("DecelerationProfile", encoding="utf-8")))
def _profile_label_for_value(self, value, options) -> str:
for option_value, option_label in options:
if option_value == value:
return tr(option_label)
return tr(options[0][1])
def _set_ev_tuning(self, state: bool):
self._params.put_bool("EVTuning", state)
if state:
self._params.put_bool("TruckTuning", False)
def _set_truck_tuning(self, state: bool):
self._params.put_bool("TruckTuning", state)
if state:
self._params.put_bool("EVTuning", False)
def _set_persist_experimental_state(self, state: bool):
sync_persist_experimental_state(self._params, self._params_memory, state)
def _set_persist_chill_state(self, state: bool):
sync_persist_chill_state(self._params, self._params_memory, state)
def _get_conditional_mode_label(self) -> str:
if self._params.get_bool("ConditionalExperimental"):
return tr("Conditional Experimental")
elif self._params.get_bool("ConditionalChill"):
return tr("Conditional Chill")
else:
return tr("OFF")
def _show_conditional_mode_selector(self):
options = ["OFF", "Conditional Experimental", "Conditional Chill"]
current = self._get_conditional_mode_label()
def on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
if dialog.selection == "OFF":
self._params.put_bool("ConditionalExperimental", False)
self._params.put_bool("ConditionalChill", False)
elif dialog.selection == "Conditional Experimental":
self._params.put_bool("ConditionalExperimental", True)
self._params.put_bool("ConditionalChill", False)
elif dialog.selection == "Conditional Chill":
self._params.put_bool("ConditionalExperimental", False)
self._params.put_bool("ConditionalChill", True)
dialog = MultiOptionDialog(tr("Conditional Drive Mode"), options, current, callback=on_select)
gui_app.push_widget(dialog)
def _reset_curve_data(self):
def on_close(res):
if res == DialogResult.CONFIRM:
self._params.put_float("CalibratedLateralAcceleration", 2.00)
self._params.remove("CalibrationProgress")
self._params.remove("CurvatureData")
gui_app.push_widget(ConfirmDialog(tr_noop("Reset Curve Data?"), tr_noop("Confirm"), callback=on_close))
def _reset_profile(self, profile: str):
def on_close(res):
if res == DialogResult.CONFIRM:
for key in ["Follow", "FollowHigh", "JerkAcceleration", "JerkDeceleration", "JerkDanger", "JerkSpeedDecrease", "JerkSpeed"]:
self._params.remove(profile + key)
gui_app.push_widget(ConfirmDialog(tr_noop("Reset to Defaults?"), tr_noop("Confirm"), callback=on_close))
def _is_metric(self) -> bool:
return self._params.get_bool("IsMetric")
# Speed-typed int params stored in the current unit (mph or km/h); rescaled
# when IsMetric flips so the numeric value stays correct in the new unit.
_SPEED_RESCALE_KEYS = (
"Offset1", "Offset2", "Offset3", "Offset4", "Offset5", "Offset6", "Offset7",
"CustomCruise", "CustomCruiseLong", "SetSpeedOffset",
"CESpeed", "CESpeedLead", "CESignalSpeed",
"CCMSpeed", "CCMSpeedLead", "CCMSetSpeedMargin",
)
# Distance-typed int params stored in the current unit (ft or m); rescaled
# when IsMetric flips so the numeric value stays correct in the new unit.
_DISTANCE_RESCALE_KEYS = (
"IncreasedStoppedDistance",
"IncreasedStoppedDistanceLowVisibility",
"IncreasedStoppedDistanceRain",
"IncreasedStoppedDistanceRainStorm",
"IncreasedStoppedDistanceSnow",
)
def _maybe_rescale_on_metric_change(self) -> None:
"""When IsMetric flips, recompute stored speed- and distance-typed int
params so they read correctly in the new unit. The first call (no prior
state) is a no-op so the user's saved values aren't rewritten on boot."""
current = self._is_metric()
# Update offset row titles dynamically to reflect correct unit-specific bounds (parity with C++).
ranges_metric = ["0-29", "30-49", "50-59", "60-79", "80-99", "100-119", "120-140"]
ranges_imperial = ["0-24", "25-34", "35-44", "45-54", "55-64", "65-74", "75-99"]
unit = "km/h" if current else "mph"
ranges = ranges_metric if current else ranges_imperial
for i, row in enumerate(self._slc_offset_rows):
row.title = f"Speed Offset ({ranges[i]} {unit})"
last = self._last_is_metric
self._last_is_metric = current
if last is None or last == current:
return
speed_factor = CV.MPH_TO_KPH if current else CV.KPH_TO_MPH
for key in self._SPEED_RESCALE_KEYS:
self._params.put_int(key, int(round(self._params.get_int(key) * speed_factor)))
distance_factor = CV.FOOT_TO_METER if current else CV.METER_TO_FOOT
for key in self._DISTANCE_RESCALE_KEYS:
self._params.put_int(key, int(round(self._params.get_int(key) * distance_factor)))
def _mapbox_available(self) -> bool:
"""SLCMapboxFiller is only meaningful when a Mapbox key is configured."""
return bool(self._params.get("MapboxSecretKey", encoding="utf-8"))
def _sources_visible(self) -> bool:
"""Abbreviated/Active-Only sub-toggles only make sense when sources are shown."""
return self._params.get_bool("SpeedLimitSources")
def _speed_unit(self) -> str:
self._maybe_rescale_on_metric_change()
return " km/h" if self._is_metric() else " mph"
def _speed_range(self) -> tuple[int, int]:
self._maybe_rescale_on_metric_change()
return (-150, 150) if self._is_metric() else (-99, 99)
# IncreasedStoppedDistance* sliders use 0..3 m in metric and 0..10 ft in
# imperial, reflecting the unit they're stored in.
def _distance_unit(self) -> str:
self._maybe_rescale_on_metric_change()
return " m" if self._is_metric() else " ft"
def _distance_range(self) -> tuple[int, int]:
self._maybe_rescale_on_metric_change()
return (0, 3) if self._is_metric() else (0, 10)
def _show_slc_offsets_category(self):
self._sub_panels["slc_offsets"] = AetherSettingsView(
self,
[SettingSection(title="", rows=self._slc_offset_rows)],
header_title=tr_noop("SLC Offsets"),
header_subtitle=tr_noop("Per-limit speed adjustments for the Speed Limit Controller."),
panel_style=PANEL_STYLE,
)
self._wire_sub_panels()
self._navigate_to("slc_offsets")
def _show_personality_profile_category(self, profile: str):
rows = self._build_personality_profile_rows(profile)
panel_name = f"profile_{profile.lower()}"
self._sub_panels[panel_name] = AetherSettingsView(
self,
[SettingSection(title="", rows=rows)],
header_title=tr_noop(f"{profile} Profile"),
header_subtitle=tr_noop("Customize follow distance and smoothness for this driving personality."),
panel_style=PANEL_STYLE,
)
self._wire_sub_panels()
self._navigate_to(panel_name)
def _show_weather_offsets_category(self, suffix: str, title: str):
rows = self._build_weather_offsets_rows(suffix)
panel_name = f"weather_{suffix.lower()}"
self._sub_panels[panel_name] = AetherSettingsView(
self,
[SettingSection(title="", rows=rows)],
header_title=tr_noop(title),
header_subtitle=tr_noop("Adjust driving parameters for this weather condition."),
panel_style=PANEL_STYLE,
)
self._wire_sub_panels()
self._navigate_to(panel_name)
def _build_personality_profile_rows(self, profile: str) -> list[SettingRow]:
follow_min = 0.5
follow_max = 2.5 if profile == "Traffic" else 3.0
p = profile
rows = [
SettingRow(f"{p}Follow", "value", tr_noop("Follow Distance"),
subtitle="",
get_value=lambda: f"{self._params.get_float(p + 'Follow'):.2f}s",
on_click=lambda: self._show_slider(p + "Follow", follow_min, follow_max, step=0.05, unit="s", value_type="float")),
]
if profile != "Traffic":
rows.append(
SettingRow(f"{p}FollowHigh", "value", tr_noop("Follow High"),
subtitle="",
get_value=lambda: f"{self._params.get_float(p + 'FollowHigh'):.2f}s",
on_click=lambda: self._show_slider(p + "FollowHigh", 1.0, 3.0, step=0.05, unit="s", value_type="float"))
)
rows.extend([
SettingRow(f"{p}JerkAccel", "value", tr_noop("Accel Smoothness"),
subtitle="",
get_value=lambda: f"{self._params.get_int(p + 'JerkAcceleration')}%",
on_click=lambda: self._show_slider(p + "JerkAcceleration", 25, 200, step=5, unit="%")),
SettingRow(f"{p}JerkDecel", "value", tr_noop("Brake Smoothness"),
subtitle="",
get_value=lambda: f"{self._params.get_int(p + 'JerkDeceleration')}%",
on_click=lambda: self._show_slider(p + "JerkDeceleration", 25, 200, step=5, unit="%")),
SettingRow(f"{p}JerkDanger", "value", tr_noop("Safety Gap Bias"),
subtitle="",
get_value=lambda: f"{self._params.get_int(p + 'JerkDanger')}%",
on_click=lambda: self._show_slider(p + "JerkDanger", 25, 200, step=5, unit="%")),
SettingRow(f"{p}JerkSpeedDec", "value", tr_noop("Slowdown Response"),
subtitle="",
get_value=lambda: f"{self._params.get_int(p + 'JerkSpeedDecrease')}%",
on_click=lambda: self._show_slider(p + "JerkSpeedDecrease", 25, 200, step=5, unit="%")),
SettingRow(f"{p}JerkSpeed", "value", tr_noop("Speed-Up Response"),
subtitle="",
get_value=lambda: f"{self._params.get_int(p + 'JerkSpeed')}%",
on_click=lambda: self._show_slider(p + "JerkSpeed", 25, 200, step=5, unit="%")),
SettingRow(f"{p}Reset", "action", tr_noop("Reset to Defaults"),
subtitle="",
action_text=tr_noop("Reset"),
action_danger=True,
on_click=lambda: self._reset_profile(p)),
])
return rows
def _build_weather_offsets_rows(self, suffix: str) -> list[SettingRow]:
s = suffix
return [
SettingRow(f"Follow{s}", "value", tr_noop("Following Distance"),
subtitle="",
get_value=lambda: f"+{self._params.get_int('IncreaseFollowing' + s)}s",
on_click=lambda: self._show_slider("IncreaseFollowing" + s, 0, 3, step=0.5, unit="s")),
SettingRow(f"StoppedDist{s}", "value", tr_noop("Stopped Distance"),
subtitle="",
get_value=lambda: f"+{self._params.get_int('IncreasedStoppedDistance' + s)}{self._distance_unit()}",
on_click=lambda: self._show_slider("IncreasedStoppedDistance" + s, *self._distance_range(),
unit=self._distance_unit())),
SettingRow(f"ReduceAccel{s}", "value", tr_noop("Reduce Accel"),
subtitle="",
get_value=lambda: f"{self._params.get_int('ReduceAcceleration' + s)}%",
on_click=lambda: self._show_slider("ReduceAcceleration" + s, 0, 99, unit="%")),
SettingRow(f"ReduceLateral{s}", "value", tr_noop("Reduce Curve Speed"),
subtitle="",
get_value=lambda: f"{self._params.get_int('ReduceLateralAcceleration' + s)}%",
on_click=lambda: self._show_slider("ReduceLateralAcceleration" + s, 0, 99, unit="%")),
]
def _set_weather_key(self):
options = ["ADD", "REMOVE"]
def on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
if dialog.selection == "ADD":
def on_key(res, text):
if res == DialogResult.CONFIRM:
self._params.put("WeatherToken", text)
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title(tr_noop("Weather API Key"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_key(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
elif dialog.selection == "REMOVE":
def on_confirm(res):
if res == DialogResult.CONFIRM:
self._params.remove("WeatherToken")
gui_app.push_widget(ConfirmDialog(tr_noop("Remove API Key?"), tr_noop("Confirm"), callback=on_confirm))
dialog = MultiOptionDialog(tr_noop("Weather API Key"), options, "ADD", callback=on_select)
gui_app.push_widget(dialog)