mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-15 03:53:58 +08:00
We do a little vibe coding
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,425 @@
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog, DialogResult
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ButtonAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonControl,
|
||||
FrogPilotButtonToggleControl,
|
||||
FrogPilotConfirmationDialog,
|
||||
FrogPilotManageControl,
|
||||
FrogPilotParamValueControl,
|
||||
)
|
||||
|
||||
DEVICE_MANAGEMENT_KEYS = {
|
||||
"DeviceShutdown",
|
||||
"HigherBitrate",
|
||||
"IncreaseThermalLimits",
|
||||
"LowVoltageShutdown",
|
||||
"NoLogging",
|
||||
"NoUploads",
|
||||
"UseKonikServer",
|
||||
}
|
||||
|
||||
SCREEN_KEYS = {
|
||||
"ScreenBrightness",
|
||||
"ScreenBrightnessOnroad",
|
||||
"ScreenRecorder",
|
||||
"ScreenTimeout",
|
||||
"ScreenTimeoutOnroad",
|
||||
"StandbyMode",
|
||||
}
|
||||
|
||||
NOT_VETTED_PATH = Path("/data/openpilot/not_vetted")
|
||||
USE_HD_PATH = Path("/cache/use_HD")
|
||||
USE_KONIK_PATH = Path("/cache/use_konik")
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
DEVICE_MANAGEMENT = 1
|
||||
SCREEN = 2
|
||||
|
||||
|
||||
def build_shutdown_labels():
|
||||
labels = {}
|
||||
for i in range(34):
|
||||
if i == 0:
|
||||
labels[i] = "5 mins"
|
||||
elif i <= 3:
|
||||
labels[i] = f"{i * 15} mins"
|
||||
elif i == 4:
|
||||
labels[i] = "1 hour"
|
||||
else:
|
||||
labels[i] = f"{i - 3} hours"
|
||||
return labels
|
||||
|
||||
|
||||
def build_brightness_labels(include_off=False):
|
||||
labels = {}
|
||||
if include_off:
|
||||
labels[0] = "Screen Off"
|
||||
for i in range(1, 101):
|
||||
labels[i] = f"{i}%"
|
||||
labels[101] = "Auto"
|
||||
return labels
|
||||
|
||||
|
||||
class FrogPilotDevicePanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._is_recording = False
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._started = False
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# Pending dialog action tracking
|
||||
self._pending_action = None # "warning_toggle", "reboot_toggle"
|
||||
self._pending_data = {}
|
||||
|
||||
self._build_main_panel()
|
||||
self._build_device_management_panel()
|
||||
self._build_screen_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_main_panel(self):
|
||||
self._device_management_control = FrogPilotManageControl(
|
||||
"DeviceManagement",
|
||||
"Device Settings",
|
||||
"<b>Settings that control how the device runs, powers off, and manages driving data.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_device.png",
|
||||
)
|
||||
self._device_management_control.set_manage_callback(self._open_device_management)
|
||||
|
||||
self._screen_management_control = FrogPilotManageControl(
|
||||
"ScreenManagement",
|
||||
"Screen Settings",
|
||||
"<b>Settings that control screen brightness, screen recording, and timeout duration.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_light.png",
|
||||
)
|
||||
self._screen_management_control.set_manage_callback(self._open_screen_panel)
|
||||
|
||||
main_items = [
|
||||
self._device_management_control,
|
||||
self._screen_management_control,
|
||||
]
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_device_management_panel(self):
|
||||
shutdown_labels = build_shutdown_labels()
|
||||
self._device_shutdown_control = FrogPilotParamValueControl(
|
||||
"DeviceShutdown",
|
||||
"Device Shutdown Timer",
|
||||
"<b>Keep the device on for the set amount of time after a drive</b> before it shuts down automatically.",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=33,
|
||||
value_labels=shutdown_labels,
|
||||
)
|
||||
|
||||
self._no_logging_item = ListItem(
|
||||
title="Disable Logging",
|
||||
description="<b>WARNING: This will prevent your drives from being recorded and all data will be unobtainable!</b><br><br><b>Prevent the device from saving driving data.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("NoLogging"),
|
||||
callback=lambda state: self._on_warning_toggle("NoLogging", state, "This will prevent your drives from being recorded. Are you sure?"),
|
||||
),
|
||||
)
|
||||
|
||||
self._no_uploads_control = FrogPilotButtonToggleControl(
|
||||
"NoUploads",
|
||||
"Disable Uploads",
|
||||
"<b>WARNING: This will prevent your drives from being uploaded to comma connect which will impact debugging and official support from comma!</b><br><br><b>Prevent the device from uploading driving data.</b>",
|
||||
"",
|
||||
button_params=["DisableOnroadUploads"],
|
||||
button_texts=["Disable Onroad Only"],
|
||||
)
|
||||
self._no_uploads_control.set_toggle_callback(lambda state: self._on_warning_toggle("NoUploads", state, "This will prevent uploads to comma connect. Are you sure?"))
|
||||
self._no_uploads_control.set_button_click_callback(lambda _: self._update_toggles())
|
||||
|
||||
self._higher_bitrate_item = ListItem(
|
||||
title="High-Quality Recording",
|
||||
description="<b>Save drive footage in higher video quality.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HigherBitrate"),
|
||||
callback=lambda state: self._on_reboot_toggle("HigherBitrate", state, USE_HD_PATH),
|
||||
),
|
||||
)
|
||||
|
||||
self._low_voltage_control = FrogPilotParamValueControl(
|
||||
"LowVoltageShutdown",
|
||||
"Low-Voltage Cutoff",
|
||||
"<b>While parked, if the battery voltage falls below the set level, the device shuts down</b> to prevent excessive battery drain.",
|
||||
"",
|
||||
min_value=11.8,
|
||||
max_value=12.5,
|
||||
label=" volts",
|
||||
interval=0.1,
|
||||
)
|
||||
|
||||
self._thermal_limits_item = ListItem(
|
||||
title="Raise Temperature Limits",
|
||||
description="<b>WARNING: Running at higher temperatures may damage your device!</b><br><br><b>Allow the device to run at higher temperatures</b> before throttling or shutting down. Use only if you understand the risks!",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("IncreaseThermalLimits"),
|
||||
callback=lambda state: self._on_warning_toggle("IncreaseThermalLimits", state, "This may damage your device. Are you sure?"),
|
||||
),
|
||||
)
|
||||
|
||||
self._use_konik_item = ListItem(
|
||||
title="Use Konik Server",
|
||||
description="<b>Upload driving data to \"stable.konik.ai\" instead of \"connect.comma.ai\".</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("UseKonikServer") or NOT_VETTED_PATH.is_file(),
|
||||
callback=lambda state: self._on_reboot_toggle("UseKonikServer", state, USE_KONIK_PATH),
|
||||
enabled=lambda: not NOT_VETTED_PATH.is_file(),
|
||||
),
|
||||
)
|
||||
|
||||
device_items = [
|
||||
self._device_shutdown_control,
|
||||
self._no_logging_item,
|
||||
self._no_uploads_control,
|
||||
self._higher_bitrate_item,
|
||||
self._low_voltage_control,
|
||||
self._thermal_limits_item,
|
||||
self._use_konik_item,
|
||||
]
|
||||
|
||||
self._toggles["DeviceShutdown"] = self._device_shutdown_control
|
||||
self._toggles["NoLogging"] = self._no_logging_item
|
||||
self._toggles["NoUploads"] = self._no_uploads_control
|
||||
self._toggles["HigherBitrate"] = self._higher_bitrate_item
|
||||
self._toggles["LowVoltageShutdown"] = self._low_voltage_control
|
||||
self._toggles["IncreaseThermalLimits"] = self._thermal_limits_item
|
||||
self._toggles["UseKonikServer"] = self._use_konik_item
|
||||
|
||||
self._device_management_scroller = Scroller(device_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_screen_panel(self):
|
||||
offroad_brightness_labels = build_brightness_labels(include_off=False)
|
||||
self._screen_brightness_control = FrogPilotParamValueControl(
|
||||
"ScreenBrightness",
|
||||
"Screen Brightness (Offroad)",
|
||||
"<b>The screen brightness while not driving.</b>",
|
||||
"",
|
||||
min_value=1,
|
||||
max_value=101,
|
||||
value_labels=offroad_brightness_labels,
|
||||
fast_increase=True,
|
||||
)
|
||||
self._screen_brightness_control.set_value_changed_callback(self._on_offroad_brightness_changed)
|
||||
|
||||
onroad_brightness_labels = build_brightness_labels(include_off=True)
|
||||
self._screen_brightness_onroad_control = FrogPilotParamValueControl(
|
||||
"ScreenBrightnessOnroad",
|
||||
"Screen Brightness (Onroad)",
|
||||
"<b>The screen brightness while driving.</b>",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=101,
|
||||
value_labels=onroad_brightness_labels,
|
||||
fast_increase=True,
|
||||
)
|
||||
self._screen_brightness_onroad_control.set_value_changed_callback(self._on_onroad_brightness_changed)
|
||||
|
||||
self._screen_recorder_control = FrogPilotButtonControl(
|
||||
"ScreenRecorder",
|
||||
"Screen Recorder",
|
||||
"<b>Add a button to the driving screen to record the display.</b>",
|
||||
"",
|
||||
button_texts=["Start Recording", "Stop Recording"],
|
||||
checkable=True,
|
||||
)
|
||||
self._screen_recorder_control.set_button_click_callback(self._on_screen_recorder_click)
|
||||
self._screen_recorder_control.set_visible_button(1, False)
|
||||
|
||||
self._screen_timeout_control = FrogPilotParamValueControl(
|
||||
"ScreenTimeout",
|
||||
"Screen Timeout (Offroad)",
|
||||
"<b>How long the screen stays on after being tapped while not driving.</b>",
|
||||
"",
|
||||
min_value=5,
|
||||
max_value=60,
|
||||
label=" seconds",
|
||||
interval=5,
|
||||
)
|
||||
|
||||
self._screen_timeout_onroad_control = FrogPilotParamValueControl(
|
||||
"ScreenTimeoutOnroad",
|
||||
"Screen Timeout (Onroad)",
|
||||
"<b>How long the screen stays on after being tapped while driving.</b>",
|
||||
"",
|
||||
min_value=5,
|
||||
max_value=60,
|
||||
label=" seconds",
|
||||
interval=5,
|
||||
)
|
||||
|
||||
self._standby_mode_item = ListItem(
|
||||
title="Standby Mode",
|
||||
description="<b>Turn the screen off while driving and automatically wake it up for alerts or engagement state changes.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("StandbyMode"),
|
||||
callback=lambda state: self._simple_toggle("StandbyMode", state),
|
||||
),
|
||||
)
|
||||
|
||||
screen_items = [
|
||||
self._screen_brightness_control,
|
||||
self._screen_brightness_onroad_control,
|
||||
self._screen_recorder_control,
|
||||
self._screen_timeout_control,
|
||||
self._screen_timeout_onroad_control,
|
||||
self._standby_mode_item,
|
||||
]
|
||||
|
||||
self._toggles["ScreenBrightness"] = self._screen_brightness_control
|
||||
self._toggles["ScreenBrightnessOnroad"] = self._screen_brightness_onroad_control
|
||||
self._toggles["ScreenRecorder"] = self._screen_recorder_control
|
||||
self._toggles["ScreenTimeout"] = self._screen_timeout_control
|
||||
self._toggles["ScreenTimeoutOnroad"] = self._screen_timeout_onroad_control
|
||||
self._toggles["StandbyMode"] = self._standby_mode_item
|
||||
|
||||
self._screen_scroller = Scroller(screen_items, line_separator=True, spacing=0)
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_warning_toggle(self, param: str, state: bool, warning_message: str):
|
||||
if state:
|
||||
self._pending_action = "warning_toggle"
|
||||
self._pending_data = {"param": param}
|
||||
gui_app.set_modal_overlay(ConfirmDialog(warning_message, "Confirm", "Cancel"))
|
||||
else:
|
||||
self._params.put_bool(param, False)
|
||||
update_frogpilot_toggles()
|
||||
self._update_toggles()
|
||||
|
||||
def _on_reboot_toggle(self, param: str, state: bool, cache_path: Path):
|
||||
self._params.put_bool(param, state)
|
||||
|
||||
if state:
|
||||
cache_path.touch(exist_ok=True)
|
||||
else:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
self._pending_action = "reboot_toggle"
|
||||
self._pending_data = {}
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Reboot required to take effect.",
|
||||
"Reboot Now",
|
||||
"Reboot Later",
|
||||
))
|
||||
|
||||
def handle_dialog_result(self, result: DialogResult, selection: str = ""):
|
||||
"""Handle dialog results for pending actions."""
|
||||
action = self._pending_action
|
||||
self._pending_action = None
|
||||
|
||||
if action == "warning_toggle":
|
||||
if result == DialogResult.CONFIRM:
|
||||
param = self._pending_data.get("param")
|
||||
if param:
|
||||
self._params.put_bool(param, True)
|
||||
update_frogpilot_toggles()
|
||||
self._update_toggles()
|
||||
self._pending_data = {}
|
||||
|
||||
elif action == "reboot_toggle":
|
||||
if result == DialogResult.CONFIRM:
|
||||
HARDWARE.reboot()
|
||||
self._pending_data = {}
|
||||
|
||||
def _on_offroad_brightness_changed(self, value: float):
|
||||
if not self._started:
|
||||
brightness = int(value) if value <= 100 else 50
|
||||
HARDWARE.set_brightness(brightness)
|
||||
|
||||
def _on_onroad_brightness_changed(self, value: float):
|
||||
if self._started:
|
||||
brightness = int(value) if value <= 100 else 50
|
||||
HARDWARE.set_brightness(brightness)
|
||||
|
||||
def _on_screen_recorder_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
# Start Recording - enable the screen recording environment variable
|
||||
self._is_recording = True
|
||||
self._screen_recorder_control.set_checked_button(1)
|
||||
self._screen_recorder_control.set_visible_button(0, False)
|
||||
self._screen_recorder_control.set_visible_button(1, True)
|
||||
|
||||
# Set params to trigger screen recording
|
||||
self._params_memory.put_bool("RecordScreen", True)
|
||||
else:
|
||||
# Stop Recording - disable the screen recording
|
||||
self._is_recording = False
|
||||
self._screen_recorder_control.clear_checked_buttons()
|
||||
self._screen_recorder_control.set_visible_button(0, True)
|
||||
self._screen_recorder_control.set_visible_button(1, False)
|
||||
|
||||
# Clear params to stop screen recording
|
||||
self._params_memory.put_bool("RecordScreen", False)
|
||||
|
||||
def _open_device_management(self):
|
||||
self._current_panel = SubPanel.DEVICE_MANAGEMENT
|
||||
|
||||
def _open_screen_panel(self):
|
||||
self._current_panel = SubPanel.SCREEN
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
|
||||
device_management_enabled = self._params.get_bool("DeviceManagement")
|
||||
no_uploads_enabled = self._params.get_bool("NoUploads")
|
||||
disable_onroad_only = self._params.get_bool("DisableOnroadUploads")
|
||||
|
||||
higher_bitrate_visible = device_management_enabled and no_uploads_enabled and not disable_onroad_only
|
||||
if hasattr(self._higher_bitrate_item, 'set_visible'):
|
||||
self._higher_bitrate_item.set_visible(higher_bitrate_visible)
|
||||
|
||||
if NOT_VETTED_PATH.is_file():
|
||||
self._params.put_bool("UseKonikServer", True)
|
||||
|
||||
def _update_state(self):
|
||||
self._started = ui_state.started
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_panel == SubPanel.DEVICE_MANAGEMENT:
|
||||
self._device_management_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.SCREEN:
|
||||
self._screen_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,407 @@
|
||||
import json
|
||||
|
||||
from cereal import car, custom, log, messaging
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item, multiple_button_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import (
|
||||
TUNING_LEVELS,
|
||||
nnff_supported,
|
||||
update_frogpilot_toggles,
|
||||
)
|
||||
from openpilot.frogpilot.ui.layouts.settings.data_settings import FrogPilotDataPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.device_settings import FrogPilotDevicePanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.lateral_settings import FrogPilotLateralPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.longitudinal_settings import FrogPilotLongitudinalPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.model_settings import FrogPilotModelPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.sounds_settings import FrogPilotSoundsPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.theme_settings import FrogPilotThemePanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.utilities import FrogPilotUtilitiesPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.vehicle_settings import FrogPilotVehiclesPanel
|
||||
from openpilot.frogpilot.ui.layouts.settings.visual_settings import FrogPilotVisualsPanel
|
||||
|
||||
TUNING_BUTTON_WIDTH = 180
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
NONE = 0
|
||||
DATA = 1
|
||||
DEVICE = 2
|
||||
LATERAL = 3
|
||||
LONGITUDINAL = 4
|
||||
MODEL = 5
|
||||
SOUNDS = 6
|
||||
THEME = 7
|
||||
UTILITIES = 8
|
||||
VEHICLES = 9
|
||||
VISUALS = 10
|
||||
|
||||
|
||||
class FrogPilotLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
|
||||
self._can_use_pedal = False
|
||||
self._can_use_sdsu = False
|
||||
self._car_make = ""
|
||||
self._car_model = ""
|
||||
self._current_subpanel = SubPanel.NONE
|
||||
self._force_open_descriptions = False
|
||||
self._friction = 0.0
|
||||
self._frogpilot_toggle_levels = {}
|
||||
self._has_alpha_longitudinal = False
|
||||
self._has_auto_tune = True
|
||||
self._has_bsm = True
|
||||
self._has_dash_speed_limits = True
|
||||
self._has_nnff_log = True
|
||||
self._has_openpilot_longitudinal = True
|
||||
self._has_pcm_cruise = False
|
||||
self._has_pedal = False
|
||||
self._has_radar = True
|
||||
self._has_sdsu = False
|
||||
self._has_sng = False
|
||||
self._has_zss = False
|
||||
self._is_angle_car = False
|
||||
self._is_bolt = False
|
||||
self._is_frogs_go_moo = False
|
||||
self._is_gm = True
|
||||
self._is_hkg = True
|
||||
self._is_hkg_canfd = True
|
||||
self._is_subaru = False
|
||||
self._is_torque_car = False
|
||||
self._is_toyota = True
|
||||
self._is_tsk = False
|
||||
self._is_volt = True
|
||||
self._lat_accel_factor = 0.0
|
||||
self._lkas_allowed_for_aol = False
|
||||
self._longitudinal_actuator_delay = 0.0
|
||||
self._openpilot_longitudinal_control_disabled = False
|
||||
self._shown_descriptions = {}
|
||||
self._start_accel = 0.0
|
||||
self._steer_actuator_delay = 0.0
|
||||
self._steer_kp = 1.0
|
||||
self._steer_ratio = 0.0
|
||||
self._stop_accel = 0.0
|
||||
self._stopping_decel_rate = 0.0
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
self._v_ego_starting = 0.0
|
||||
self._v_ego_stopping = 0.0
|
||||
|
||||
self._subpanels = {
|
||||
SubPanel.DATA: FrogPilotDataPanel(),
|
||||
SubPanel.DEVICE: FrogPilotDevicePanel(),
|
||||
SubPanel.LATERAL: FrogPilotLateralPanel(),
|
||||
SubPanel.LONGITUDINAL: FrogPilotLongitudinalPanel(),
|
||||
SubPanel.MODEL: FrogPilotModelPanel(),
|
||||
SubPanel.SOUNDS: FrogPilotSoundsPanel(),
|
||||
SubPanel.THEME: FrogPilotThemePanel(),
|
||||
SubPanel.UTILITIES: FrogPilotUtilitiesPanel(),
|
||||
SubPanel.VEHICLES: FrogPilotVehiclesPanel(),
|
||||
SubPanel.VISUALS: FrogPilotVisualsPanel(),
|
||||
}
|
||||
|
||||
self._load_shown_descriptions()
|
||||
self._load_toggle_levels()
|
||||
self._check_force_open_descriptions()
|
||||
|
||||
self._tuning_level_item = multiple_button_item(
|
||||
lambda: "Tuning Level",
|
||||
lambda: (
|
||||
"Choose your tuning level. Lower levels keep it simple; higher levels unlock more toggles for finer control.\n\n"
|
||||
"Minimal - Ideal for those who prefer simplicity or ease of use\n"
|
||||
"Standard - Recommended for most users for a balanced experience\n"
|
||||
"Advanced - Fine-tuning for experienced users\n"
|
||||
"Developer - Highly customizable settings for seasoned enthusiasts"
|
||||
),
|
||||
buttons=[lambda: "Minimal", lambda: "Standard", lambda: "Advanced", lambda: "Developer"],
|
||||
button_width=TUNING_BUTTON_WIDTH,
|
||||
selected_index=self._tuning_level,
|
||||
callback=self._on_tuning_level_changed,
|
||||
icon="../../frogpilot/assets/toggle_icons/icon_tuning.png",
|
||||
)
|
||||
|
||||
self._sound_panel_item = button_item(
|
||||
lambda: "Alerts and Sounds",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Adjust alert volumes and enable custom notifications.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.SOUNDS),
|
||||
)
|
||||
|
||||
self._model_panel_item = button_item(
|
||||
lambda: "Driving Model",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Select and configure driving models.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.MODEL),
|
||||
)
|
||||
|
||||
self._longitudinal_panel_item = button_item(
|
||||
lambda: "Gas / Brake",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Fine-tune acceleration and braking controls.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.LONGITUDINAL),
|
||||
)
|
||||
|
||||
self._lateral_panel_item = button_item(
|
||||
lambda: "Steering",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Fine-tune steering controls.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.LATERAL),
|
||||
)
|
||||
|
||||
self._data_panel_item = button_item(
|
||||
lambda: "Data",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Manage data and backups.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.DATA),
|
||||
)
|
||||
|
||||
self._device_panel_item = button_item(
|
||||
lambda: "Device Controls",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Configure device settings and screen options.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.DEVICE),
|
||||
)
|
||||
|
||||
self._utilities_panel_item = button_item(
|
||||
lambda: "Utilities",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Tools to keep FrogPilot running smoothly.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.UTILITIES),
|
||||
)
|
||||
|
||||
self._visuals_panel_item = button_item(
|
||||
lambda: "Appearance",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Customize the look of the driving screen.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.VISUALS),
|
||||
)
|
||||
|
||||
self._theme_panel_item = button_item(
|
||||
lambda: "Theme",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Customize themes and colors.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.THEME),
|
||||
)
|
||||
|
||||
self._vehicles_panel_item = button_item(
|
||||
lambda: "Vehicle Settings",
|
||||
lambda: "MANAGE",
|
||||
lambda: "<b>Configure car-specific options.</b>",
|
||||
callback=lambda: self._open_subpanel(SubPanel.VEHICLES),
|
||||
)
|
||||
|
||||
items = [
|
||||
self._tuning_level_item,
|
||||
self._sound_panel_item,
|
||||
self._model_panel_item,
|
||||
self._longitudinal_panel_item,
|
||||
self._lateral_panel_item,
|
||||
self._data_panel_item,
|
||||
self._device_panel_item,
|
||||
self._utilities_panel_item,
|
||||
self._visuals_panel_item,
|
||||
self._theme_panel_item,
|
||||
self._vehicles_panel_item,
|
||||
]
|
||||
|
||||
self._main_scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_variables)
|
||||
|
||||
def _load_shown_descriptions(self):
|
||||
try:
|
||||
data = self._params.get("ShownToggleDescriptions")
|
||||
if data:
|
||||
self._shown_descriptions = json.loads(data)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
self._shown_descriptions = {}
|
||||
|
||||
def _save_shown_descriptions(self):
|
||||
self._params.put_nonblocking("ShownToggleDescriptions", json.dumps(self._shown_descriptions))
|
||||
|
||||
def _load_toggle_levels(self):
|
||||
keys = self._params.all_keys()
|
||||
for key in keys:
|
||||
key_str = key.decode() if isinstance(key, bytes) else key
|
||||
self._frogpilot_toggle_levels[key_str] = self._params.get_tuning_level(key)
|
||||
|
||||
def _check_force_open_descriptions(self):
|
||||
class_name = "FrogPilotLayout"
|
||||
if not self._shown_descriptions.get(class_name, False):
|
||||
self._force_open_descriptions = True
|
||||
|
||||
def _on_tuning_level_changed(self, level: int):
|
||||
self._tuning_level = level
|
||||
self._params.put_int("TuningLevel", level)
|
||||
update_frogpilot_toggles()
|
||||
self._update_panel_visibility()
|
||||
|
||||
if level == TUNING_LEVELS["DEVELOPER"]:
|
||||
gui_app.set_modal_overlay(alert_dialog(
|
||||
"WARNING: These settings are risky and can drastically change how openpilot drives. "
|
||||
"Only change if you fully understand what they do!"
|
||||
))
|
||||
|
||||
def _open_subpanel(self, subpanel: SubPanel):
|
||||
if self._current_subpanel != SubPanel.NONE:
|
||||
self._subpanels[self._current_subpanel].hide_event()
|
||||
self._current_subpanel = subpanel
|
||||
if subpanel != SubPanel.NONE:
|
||||
self._subpanels[subpanel].show_event()
|
||||
|
||||
def _close_subpanel(self):
|
||||
if self._current_subpanel != SubPanel.NONE:
|
||||
self._subpanels[self._current_subpanel].hide_event()
|
||||
self._current_subpanel = SubPanel.NONE
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_subpanel != SubPanel.NONE:
|
||||
self._subpanels[self._current_subpanel].render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
|
||||
class_name = "FrogPilotLayout"
|
||||
if not self._shown_descriptions.get(class_name, False):
|
||||
self._shown_descriptions[class_name] = True
|
||||
self._save_shown_descriptions()
|
||||
|
||||
if self._force_open_descriptions:
|
||||
self._force_open_descriptions = False
|
||||
gui_app.set_modal_overlay(alert_dialog(
|
||||
"All toggle descriptions are currently expanded. You can tap a toggle's name to open or close its description at any time!"
|
||||
))
|
||||
|
||||
if self._current_subpanel != SubPanel.NONE:
|
||||
self._subpanels[self._current_subpanel].show_event()
|
||||
|
||||
self._update_variables()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._current_subpanel != SubPanel.NONE:
|
||||
self._subpanels[self._current_subpanel].hide_event()
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _update_variables(self):
|
||||
try:
|
||||
car_params_bytes = self._params.get("CarParamsPersistent")
|
||||
if car_params_bytes:
|
||||
CP = messaging.log_from_bytes(car_params_bytes, car.CarParams)
|
||||
|
||||
self._car_make = CP.brand
|
||||
self._car_model = CP.carFingerprint
|
||||
|
||||
self._friction = CP.lateralTuning.torque.friction
|
||||
self._has_alpha_longitudinal = CP.alphaLongitudinalAvailable
|
||||
self._has_bsm = CP.enableBsm
|
||||
self._has_dash_speed_limits = self._car_make in ("ford", "hyundai", "toyota")
|
||||
self._has_nnff_log = nnff_supported(self._car_model)
|
||||
self._has_openpilot_longitudinal = CP.openpilotLongitudinalControl
|
||||
self._has_pcm_cruise = CP.pcmCruise
|
||||
self._has_pedal = CP.enableGasInterceptorDEPRECATED
|
||||
self._has_radar = not CP.radarUnavailable
|
||||
self._has_sng = CP.autoResumeSng
|
||||
self._is_angle_car = CP.steerControlType == car.CarParams.SteerControlType.angle
|
||||
self._is_bolt = self._car_model in ("CHEVROLET_BOLT_CC", "CHEVROLET_BOLT_EUV")
|
||||
self._is_gm = self._car_make == "gm"
|
||||
self._is_hkg = self._car_make == "hyundai"
|
||||
self._is_subaru = self._car_make == "subaru"
|
||||
self._is_torque_car = CP.lateralTuning.which() == "torque"
|
||||
self._is_toyota = self._car_make == "toyota"
|
||||
self._is_tsk = CP.secOcRequired
|
||||
self._is_volt = self._car_model == "CHEVROLET_VOLT"
|
||||
self._lat_accel_factor = CP.lateralTuning.torque.latAccelFactor
|
||||
self._longitudinal_actuator_delay = CP.longitudinalActuatorDelay
|
||||
self._start_accel = CP.startAccel
|
||||
self._steer_actuator_delay = CP.steerActuatorDelay
|
||||
self._steer_ratio = CP.steerRatio
|
||||
self._stop_accel = CP.stopAccel
|
||||
self._stopping_decel_rate = CP.stoppingDecelRate
|
||||
self._v_ego_starting = CP.vEgoStarting
|
||||
self._v_ego_stopping = CP.vEgoStopping
|
||||
|
||||
self._update_stock_values(CP)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
fp_car_params_bytes = self._params.get("FrogPilotCarParamsPersistent")
|
||||
if fp_car_params_bytes:
|
||||
FPCP = messaging.log_from_bytes(fp_car_params_bytes, custom.FrogPilotCarParams)
|
||||
self._can_use_pedal = FPCP.canUsePedal
|
||||
self._can_use_sdsu = FPCP.canUseSDSU
|
||||
self._openpilot_longitudinal_control_disabled = FPCP.openpilotLongitudinalControlDisabled
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
ltp_bytes = self._params.get("LiveTorqueParameters")
|
||||
if ltp_bytes:
|
||||
LTP = messaging.log_from_bytes(ltp_bytes, log.LiveTorqueParametersData)
|
||||
self._has_auto_tune = LTP.useParams
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._update_panel_visibility()
|
||||
|
||||
def _update_stock_values(self, CP):
|
||||
stock_params = [
|
||||
("SteerDelayStock", "SteerDelay", self._steer_actuator_delay),
|
||||
("SteerFrictionStock", "SteerFriction", self._friction),
|
||||
("SteerKPStock", "SteerKP", self._steer_kp),
|
||||
("SteerLatAccelStock", "SteerLatAccel", self._lat_accel_factor),
|
||||
("LongitudinalActuatorDelayStock", "LongitudinalActuatorDelay", self._longitudinal_actuator_delay),
|
||||
("StartAccelStock", "StartAccel", self._start_accel),
|
||||
("SteerRatioStock", "SteerRatio", self._steer_ratio),
|
||||
("StopAccelStock", "StopAccel", self._stop_accel),
|
||||
("StoppingDecelRateStock", "StoppingDecelRate", self._stopping_decel_rate),
|
||||
("VEgoStartingStock", "VEgoStarting", self._v_ego_starting),
|
||||
("VEgoStoppingStock", "VEgoStopping", self._v_ego_stopping),
|
||||
]
|
||||
|
||||
for stock_key, user_key, new_value in stock_params:
|
||||
if new_value == 0:
|
||||
continue
|
||||
|
||||
current_stock = self._params.get_float(stock_key)
|
||||
if current_stock != new_value:
|
||||
current_user = self._params.get_float(user_key)
|
||||
if current_user == current_stock or current_stock == 0:
|
||||
self._params.put_float_nonblocking(user_key, new_value)
|
||||
self._params.put_float_nonblocking(stock_key, new_value)
|
||||
|
||||
def _update_panel_visibility(self):
|
||||
self._longitudinal_panel_item.set_visible(self._has_openpilot_longitudinal)
|
||||
|
||||
device_mgmt_level = self._frogpilot_toggle_levels.get("DeviceManagement", 0)
|
||||
screen_mgmt_level = self._frogpilot_toggle_levels.get("ScreenManagement", 0)
|
||||
self._device_panel_item.set_visible(self._tuning_level >= device_mgmt_level or self._tuning_level >= screen_mgmt_level)
|
||||
|
||||
@property
|
||||
def tuning_level(self) -> int:
|
||||
return self._tuning_level
|
||||
|
||||
@property
|
||||
def has_openpilot_longitudinal(self) -> bool:
|
||||
return self._has_openpilot_longitudinal
|
||||
|
||||
@property
|
||||
def car_make(self) -> str:
|
||||
return self._car_make
|
||||
|
||||
@property
|
||||
def car_model(self) -> str:
|
||||
return self._car_model
|
||||
@@ -0,0 +1,702 @@
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import nnff_supported, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonToggleControl,
|
||||
FrogPilotConfirmationDialog,
|
||||
FrogPilotManageControl,
|
||||
FrogPilotParamValueButtonControl,
|
||||
FrogPilotParamValueControl,
|
||||
)
|
||||
|
||||
ADVANCED_LATERAL_TUNE_KEYS = {
|
||||
"ForceAutoTune",
|
||||
"ForceAutoTuneOff",
|
||||
"ForceTorqueController",
|
||||
"SteerDelay",
|
||||
"SteerFriction",
|
||||
"SteerKP",
|
||||
"SteerLatAccel",
|
||||
"SteerRatio",
|
||||
}
|
||||
|
||||
AOL_KEYS = {
|
||||
"AlwaysOnLateralLKAS",
|
||||
"PauseAOLOnBrake",
|
||||
}
|
||||
|
||||
LANE_CHANGE_KEYS = {
|
||||
"LaneChangeTime",
|
||||
"LaneDetectionWidth",
|
||||
"MinimumLaneChangeSpeed",
|
||||
"NudgelessLaneChange",
|
||||
"OneLaneChange",
|
||||
}
|
||||
|
||||
LATERAL_TUNE_KEYS = {
|
||||
"NNFF",
|
||||
"NNFFLite",
|
||||
"TurnDesires",
|
||||
}
|
||||
|
||||
QOL_KEYS = {
|
||||
"PauseLateralSpeed",
|
||||
}
|
||||
|
||||
FOOT_TO_METER = CV.FOOT_TO_METER
|
||||
METER_TO_FOOT = CV.METER_TO_FOOT
|
||||
KM_TO_MILE = 1.0 / CV.MPH_TO_KPH
|
||||
MILE_TO_KM = CV.MPH_TO_KPH
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
ADVANCED_LATERAL_TUNE = 1
|
||||
AOL = 2
|
||||
LANE_CHANGE = 3
|
||||
LATERAL_TUNE = 4
|
||||
QOL = 5
|
||||
|
||||
|
||||
def build_lane_change_time_labels():
|
||||
labels = {}
|
||||
for i in range(51):
|
||||
val = i / 10.0
|
||||
if val == 0:
|
||||
labels[val] = "Instant"
|
||||
elif val == 1.0:
|
||||
labels[val] = "1.0 second"
|
||||
else:
|
||||
labels[val] = f"{val:.1f} seconds"
|
||||
return labels
|
||||
|
||||
|
||||
def build_imperial_speed_labels():
|
||||
labels = {}
|
||||
for i in range(100):
|
||||
labels[i] = "Off" if i == 0 else f"{i} mph"
|
||||
return labels
|
||||
|
||||
|
||||
def build_metric_speed_labels():
|
||||
labels = {}
|
||||
for i in range(151):
|
||||
labels[i] = "Off" if i == 0 else f"{i} km/h"
|
||||
return labels
|
||||
|
||||
|
||||
def build_imperial_distance_labels():
|
||||
labels = {}
|
||||
for i in range(151):
|
||||
val = i / 10.0
|
||||
if val == 0:
|
||||
labels[val] = "Off"
|
||||
elif i == 1:
|
||||
labels[val] = "1 foot"
|
||||
else:
|
||||
labels[val] = f"{val:.1f} feet"
|
||||
return labels
|
||||
|
||||
|
||||
def build_metric_distance_labels():
|
||||
labels = {}
|
||||
for i in range(51):
|
||||
val = i / 10.0
|
||||
if val == 0:
|
||||
labels[val] = "Off"
|
||||
elif i == 1:
|
||||
labels[val] = "1 meter"
|
||||
else:
|
||||
labels[val] = f"{val:.1f} meters"
|
||||
return labels
|
||||
|
||||
|
||||
class FrogPilotLateralPanel(Widget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._is_metric = False
|
||||
self._params = Params()
|
||||
self._parent = parent
|
||||
self._started = False
|
||||
self._toggles = {}
|
||||
|
||||
self._car_model = ""
|
||||
self._friction = 0.0
|
||||
self._has_auto_tune = True
|
||||
self._has_nnff_log = False
|
||||
self._is_angle_car = False
|
||||
self._is_torque_car = False
|
||||
self._lat_accel_factor = 0.0
|
||||
self._lkas_allowed_for_aol = False
|
||||
self._steer_actuator_delay = 0.0
|
||||
self._steer_kp = 1.0
|
||||
self._steer_ratio = 0.0
|
||||
self._tuning_level = 0
|
||||
|
||||
self._build_main_panel()
|
||||
self._build_advanced_lateral_tune_panel()
|
||||
self._build_aol_panel()
|
||||
self._build_lane_change_panel()
|
||||
self._build_lateral_tune_panel()
|
||||
self._build_qol_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._on_offroad_transition)
|
||||
|
||||
def _on_offroad_transition(self):
|
||||
self._is_metric = self._params.get_bool("IsMetric")
|
||||
self._update_metric()
|
||||
self._update_car_params()
|
||||
self._update_toggles()
|
||||
|
||||
def _build_main_panel(self):
|
||||
self._advanced_lateral_tune_control = FrogPilotManageControl(
|
||||
"AdvancedLateralTune",
|
||||
"Advanced Lateral Tuning",
|
||||
"<b>Advanced steering control changes to fine-tune how openpilot drives.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_advanced_lateral_tune.png",
|
||||
)
|
||||
self._advanced_lateral_tune_control.set_manage_callback(self._open_advanced_lateral_tune)
|
||||
|
||||
self._aol_control = FrogPilotManageControl(
|
||||
"AlwaysOnLateral",
|
||||
"Always On Lateral",
|
||||
"<b>openpilot's steering remains active even when the accelerator or brake pedals are pressed.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_always_on_lateral.png",
|
||||
)
|
||||
self._aol_control.set_manage_callback(self._open_aol_panel)
|
||||
|
||||
self._lane_changes_control = FrogPilotManageControl(
|
||||
"LaneChanges",
|
||||
"Lane Changes",
|
||||
"<b>Allow openpilot to change lanes.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_lane.png",
|
||||
)
|
||||
self._lane_changes_control.set_manage_callback(self._open_lane_change_panel)
|
||||
|
||||
self._lateral_tune_control = FrogPilotManageControl(
|
||||
"LateralTune",
|
||||
"Lateral Tuning",
|
||||
"<b>Miscellaneous steering control changes</b> to fine-tune how openpilot drives.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_lateral_tune.png",
|
||||
)
|
||||
self._lateral_tune_control.set_manage_callback(self._open_lateral_tune_panel)
|
||||
|
||||
self._qol_lateral_control = FrogPilotManageControl(
|
||||
"QOLLateral",
|
||||
"Quality of Life",
|
||||
"<b>Steering control changes to fine-tune how openpilot drives.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_quality_of_life.png",
|
||||
)
|
||||
self._qol_lateral_control.set_manage_callback(self._open_qol_panel)
|
||||
|
||||
main_items = [
|
||||
self._advanced_lateral_tune_control,
|
||||
self._aol_control,
|
||||
self._lane_changes_control,
|
||||
self._lateral_tune_control,
|
||||
self._qol_lateral_control,
|
||||
]
|
||||
|
||||
self._toggles["AdvancedLateralTune"] = self._advanced_lateral_tune_control
|
||||
self._toggles["AlwaysOnLateral"] = self._aol_control
|
||||
self._toggles["LaneChanges"] = self._lane_changes_control
|
||||
self._toggles["LateralTune"] = self._lateral_tune_control
|
||||
self._toggles["QOLLateral"] = self._qol_lateral_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_advanced_lateral_tune_panel(self):
|
||||
self._steer_delay_control = FrogPilotParamValueButtonControl(
|
||||
"SteerDelay",
|
||||
"Actuator Delay",
|
||||
"<b>The time between openpilot's steering command and the vehicle's response.</b> Increase if the vehicle reacts late; decrease if it feels jumpy. Auto-learned by default.",
|
||||
"",
|
||||
min_value=0.01,
|
||||
max_value=1.0,
|
||||
interval=0.01,
|
||||
button_texts=["Reset"],
|
||||
)
|
||||
self._steer_delay_control.set_button_click_callback(lambda _: self._reset_param("SteerDelay", self._steer_actuator_delay))
|
||||
|
||||
self._steer_friction_control = FrogPilotParamValueButtonControl(
|
||||
"SteerFriction",
|
||||
"Friction",
|
||||
"<b>Compensates for steering friction.</b> Increase if the wheel sticks near center; decrease if it jitters. Auto-learned by default.",
|
||||
"",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
interval=0.01,
|
||||
button_texts=["Reset"],
|
||||
)
|
||||
self._steer_friction_control.set_button_click_callback(lambda _: self._reset_param("SteerFriction", self._friction))
|
||||
|
||||
self._steer_kp_control = FrogPilotParamValueButtonControl(
|
||||
"SteerKP",
|
||||
"Kp Factor",
|
||||
"<b>How strongly openpilot corrects lane position.</b> Higher is tighter but twitchier; lower is smoother but slower. Auto-learned by default.",
|
||||
"",
|
||||
min_value=0.5,
|
||||
max_value=1.5,
|
||||
interval=0.01,
|
||||
button_texts=["Reset"],
|
||||
)
|
||||
self._steer_kp_control.set_button_click_callback(lambda _: self._reset_param("SteerKP", self._steer_kp))
|
||||
|
||||
self._steer_lat_accel_control = FrogPilotParamValueButtonControl(
|
||||
"SteerLatAccel",
|
||||
"Lateral Acceleration",
|
||||
"<b>Maps steering torque to turning response.</b> Increase for sharper turns; decrease for gentler steering. Auto-learned by default.",
|
||||
"",
|
||||
min_value=0.5,
|
||||
max_value=1.5,
|
||||
interval=0.01,
|
||||
button_texts=["Reset"],
|
||||
)
|
||||
self._steer_lat_accel_control.set_button_click_callback(lambda _: self._reset_param("SteerLatAccel", self._lat_accel_factor))
|
||||
|
||||
self._steer_ratio_control = FrogPilotParamValueButtonControl(
|
||||
"SteerRatio",
|
||||
"Steer Ratio",
|
||||
"<b>The relationship between steering wheel rotation and road wheel angle.</b> Increase if steering feels too quick or twitchy; decrease if it feels too slow or weak. Auto-learned by default.",
|
||||
"",
|
||||
min_value=5.0,
|
||||
max_value=25.0,
|
||||
interval=0.01,
|
||||
button_texts=["Reset"],
|
||||
)
|
||||
self._steer_ratio_control.set_button_click_callback(lambda _: self._reset_param("SteerRatio", self._steer_ratio))
|
||||
|
||||
self._force_auto_tune_item = ListItem(
|
||||
title="Force Auto-Tune On",
|
||||
description="<b>Force-enable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\".</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("ForceAutoTune"),
|
||||
callback=lambda state: self._on_toggle("ForceAutoTune", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._force_auto_tune_off_item = ListItem(
|
||||
title="Force Auto-Tune Off",
|
||||
description="<b>Force-disable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\" and use the set value instead.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("ForceAutoTuneOff"),
|
||||
callback=lambda state: self._on_toggle("ForceAutoTuneOff", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._force_torque_controller_item = ListItem(
|
||||
title="Force Torque Controller",
|
||||
description="<b>Use torque-based steering control instead of angle-based control for smoother lane keeping, especially in curves.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("ForceTorqueController"),
|
||||
callback=lambda state: self._on_reboot_toggle("ForceTorqueController", state),
|
||||
),
|
||||
)
|
||||
|
||||
advanced_items = [
|
||||
self._steer_delay_control,
|
||||
self._steer_friction_control,
|
||||
self._steer_kp_control,
|
||||
self._steer_lat_accel_control,
|
||||
self._steer_ratio_control,
|
||||
self._force_auto_tune_item,
|
||||
self._force_auto_tune_off_item,
|
||||
self._force_torque_controller_item,
|
||||
]
|
||||
|
||||
self._toggles["SteerDelay"] = self._steer_delay_control
|
||||
self._toggles["SteerFriction"] = self._steer_friction_control
|
||||
self._toggles["SteerKP"] = self._steer_kp_control
|
||||
self._toggles["SteerLatAccel"] = self._steer_lat_accel_control
|
||||
self._toggles["SteerRatio"] = self._steer_ratio_control
|
||||
self._toggles["ForceAutoTune"] = self._force_auto_tune_item
|
||||
self._toggles["ForceAutoTuneOff"] = self._force_auto_tune_off_item
|
||||
self._toggles["ForceTorqueController"] = self._force_torque_controller_item
|
||||
|
||||
self._advanced_lateral_tune_scroller = Scroller(advanced_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_aol_panel(self):
|
||||
self._aol_lkas_item = ListItem(
|
||||
title="Enable With LKAS",
|
||||
description="<b>Enable \"Always On Lateral\" whenever \"LKAS\" is on, even when openpilot is not engaged.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("AlwaysOnLateralLKAS"),
|
||||
callback=lambda state: self._on_toggle("AlwaysOnLateralLKAS", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._pause_aol_on_brake_control = FrogPilotParamValueControl(
|
||||
"PauseAOLOnBrake",
|
||||
"Pause on Brake Press Below",
|
||||
"<b>Pause \"Always On Lateral\" below the set speed while the brake pedal is pressed.</b>",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=99,
|
||||
fast_increase=True,
|
||||
)
|
||||
|
||||
aol_items = [
|
||||
self._aol_lkas_item,
|
||||
self._pause_aol_on_brake_control,
|
||||
]
|
||||
|
||||
self._toggles["AlwaysOnLateralLKAS"] = self._aol_lkas_item
|
||||
self._toggles["PauseAOLOnBrake"] = self._pause_aol_on_brake_control
|
||||
|
||||
self._aol_scroller = Scroller(aol_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_lane_change_panel(self):
|
||||
self._nudgeless_lane_change_item = ListItem(
|
||||
title="Automatic Lane Changes",
|
||||
description="<b>When the turn signal is on, openpilot will automatically change lanes.</b> No steering-wheel nudge required!",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("NudgelessLaneChange"),
|
||||
callback=lambda state: self._on_toggle("NudgelessLaneChange", state),
|
||||
),
|
||||
)
|
||||
|
||||
lane_change_time_labels = build_lane_change_time_labels()
|
||||
self._lane_change_time_control = FrogPilotParamValueControl(
|
||||
"LaneChangeTime",
|
||||
"Lane Change Delay",
|
||||
"<b>Delay between turn signal activation and the start of an automatic lane change.</b>",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=5,
|
||||
value_labels=lane_change_time_labels,
|
||||
interval=0.1,
|
||||
)
|
||||
|
||||
self._minimum_lane_change_speed_control = FrogPilotParamValueControl(
|
||||
"MinimumLaneChangeSpeed",
|
||||
"Minimum Lane Change Speed",
|
||||
"<b>Lowest speed at which openpilot will change lanes.</b>",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=99,
|
||||
fast_increase=True,
|
||||
)
|
||||
|
||||
self._lane_detection_width_control = FrogPilotParamValueControl(
|
||||
"LaneDetectionWidth",
|
||||
"Minimum Lane Width",
|
||||
"<b>Prevent automatic lane changes into lanes narrower than the set width.</b>",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=15,
|
||||
interval=0.1,
|
||||
fast_increase=True,
|
||||
)
|
||||
|
||||
self._one_lane_change_item = ListItem(
|
||||
title="One Lane Change Per Signal",
|
||||
description="<b>Limit automatic lane changes to one per turn-signal activation.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("OneLaneChange"),
|
||||
callback=lambda state: self._on_toggle("OneLaneChange", state),
|
||||
),
|
||||
)
|
||||
|
||||
lane_change_items = [
|
||||
self._nudgeless_lane_change_item,
|
||||
self._lane_change_time_control,
|
||||
self._minimum_lane_change_speed_control,
|
||||
self._lane_detection_width_control,
|
||||
self._one_lane_change_item,
|
||||
]
|
||||
|
||||
self._toggles["NudgelessLaneChange"] = self._nudgeless_lane_change_item
|
||||
self._toggles["LaneChangeTime"] = self._lane_change_time_control
|
||||
self._toggles["MinimumLaneChangeSpeed"] = self._minimum_lane_change_speed_control
|
||||
self._toggles["LaneDetectionWidth"] = self._lane_detection_width_control
|
||||
self._toggles["OneLaneChange"] = self._one_lane_change_item
|
||||
|
||||
self._lane_change_scroller = Scroller(lane_change_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_lateral_tune_panel(self):
|
||||
self._turn_desires_item = ListItem(
|
||||
title="Force Turn Desires Below Lane Change Speed",
|
||||
description="<b>While driving below the minimum lane change speed with an active turn signal, instruct openpilot to turn left/right.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("TurnDesires"),
|
||||
callback=lambda state: self._on_toggle("TurnDesires", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._nnff_item = ListItem(
|
||||
title="Neural Network Feedforward (NNFF)",
|
||||
description="<b>Twilsonco's \"Neural Network FeedForward\" controller.</b> Uses a trained neural network model to predict steering torque based on vehicle speed, roll, and past/future planned path data for smoother, model-based steering.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("NNFF"),
|
||||
callback=lambda state: self._on_reboot_toggle("NNFF", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._nnff_lite_item = ListItem(
|
||||
title="Neural Network Feedforward (NNFF) Lite",
|
||||
description="<b>A lightweight version of Twilsonco's \"Neural Network FeedForward\" controller.</b> Uses the \"look-ahead\" planned lateral jerk logic from the full model to help smoothen steering adjustments in curves, but does not use the full neural network for torque calculation.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("NNFFLite"),
|
||||
callback=lambda state: self._on_reboot_toggle("NNFFLite", state),
|
||||
),
|
||||
)
|
||||
|
||||
lateral_tune_items = [
|
||||
self._turn_desires_item,
|
||||
self._nnff_item,
|
||||
self._nnff_lite_item,
|
||||
]
|
||||
|
||||
self._toggles["TurnDesires"] = self._turn_desires_item
|
||||
self._toggles["NNFF"] = self._nnff_item
|
||||
self._toggles["NNFFLite"] = self._nnff_lite_item
|
||||
|
||||
self._lateral_tune_scroller = Scroller(lateral_tune_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_qol_panel(self):
|
||||
self._pause_lateral_speed_control = FrogPilotParamValueButtonControl(
|
||||
"PauseLateralSpeed",
|
||||
"Pause Steering Below",
|
||||
"<b>Pause steering below the set speed.</b>",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=99,
|
||||
fast_increase=True,
|
||||
button_params=["PauseLateralOnSignal"],
|
||||
button_texts=["Turn Signal Only"],
|
||||
)
|
||||
|
||||
qol_items = [
|
||||
self._pause_lateral_speed_control,
|
||||
]
|
||||
|
||||
self._toggles["PauseLateralSpeed"] = self._pause_lateral_speed_control
|
||||
|
||||
self._qol_scroller = Scroller(qol_items, line_separator=True, spacing=0)
|
||||
|
||||
def _on_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
self._update_toggles()
|
||||
|
||||
def _on_reboot_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
self._update_toggles()
|
||||
|
||||
if self._started:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Reboot required to take effect.",
|
||||
"Reboot Now",
|
||||
"Reboot Later",
|
||||
))
|
||||
|
||||
def _reset_param(self, param: str, default_value: float):
|
||||
def on_confirm():
|
||||
self._params.put_float(param, default_value)
|
||||
if param == "SteerDelay":
|
||||
self._steer_delay_control.refresh()
|
||||
elif param == "SteerFriction":
|
||||
self._steer_friction_control.refresh()
|
||||
elif param == "SteerKP":
|
||||
self._steer_kp_control.refresh()
|
||||
elif param == "SteerLatAccel":
|
||||
self._steer_lat_accel_control.refresh()
|
||||
elif param == "SteerRatio":
|
||||
self._steer_ratio_control.refresh()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
f"Reset to its default value ({default_value:.2f})?",
|
||||
"Reset",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def _open_advanced_lateral_tune(self):
|
||||
self._current_panel = SubPanel.ADVANCED_LATERAL_TUNE
|
||||
|
||||
def _open_aol_panel(self):
|
||||
self._current_panel = SubPanel.AOL
|
||||
|
||||
def _open_lane_change_panel(self):
|
||||
self._current_panel = SubPanel.LANE_CHANGE
|
||||
|
||||
def _open_lateral_tune_panel(self):
|
||||
self._current_panel = SubPanel.LATERAL_TUNE
|
||||
|
||||
def _open_qol_panel(self):
|
||||
self._current_panel = SubPanel.QOL
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _update_car_params(self):
|
||||
try:
|
||||
from cereal import car, messaging
|
||||
car_params_bytes = self._params.get("CarParamsPersistent")
|
||||
if car_params_bytes:
|
||||
CP = messaging.log_from_bytes(car_params_bytes, car.CarParams)
|
||||
|
||||
self._car_model = CP.carFingerprint
|
||||
self._friction = CP.lateralTuning.torque.friction
|
||||
self._has_nnff_log = nnff_supported(self._car_model)
|
||||
self._is_angle_car = CP.steerControlType == car.CarParams.SteerControlType.angle
|
||||
self._is_torque_car = CP.lateralTuning.which() == "torque"
|
||||
self._lat_accel_factor = CP.lateralTuning.torque.latAccelFactor
|
||||
self._steer_actuator_delay = CP.steerActuatorDelay
|
||||
self._steer_ratio = CP.steerRatio
|
||||
|
||||
self._update_steering_control_titles()
|
||||
self._update_steering_control_ranges()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
from cereal import log
|
||||
ltp_bytes = self._params.get("LiveTorqueParameters")
|
||||
if ltp_bytes:
|
||||
from cereal import messaging
|
||||
LTP = messaging.log_from_bytes(ltp_bytes, log.LiveTorqueParametersData)
|
||||
self._has_auto_tune = LTP.useParams
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _update_steering_control_titles(self):
|
||||
if self._steer_actuator_delay != 0:
|
||||
self._steer_delay_control.set_title(f"Actuator Delay (Default: {self._steer_actuator_delay:.2f})")
|
||||
if self._friction != 0:
|
||||
self._steer_friction_control.set_title(f"Friction (Default: {self._friction:.2f})")
|
||||
if self._steer_kp != 0:
|
||||
self._steer_kp_control.set_title(f"Kp Factor (Default: {self._steer_kp:.2f})")
|
||||
if self._lat_accel_factor != 0:
|
||||
self._steer_lat_accel_control.set_title(f"Lateral Acceleration (Default: {self._lat_accel_factor:.2f})")
|
||||
if self._steer_ratio != 0:
|
||||
self._steer_ratio_control.set_title(f"Steer Ratio (Default: {self._steer_ratio:.2f})")
|
||||
|
||||
def _update_steering_control_ranges(self):
|
||||
if self._steer_kp > 0:
|
||||
self._steer_kp_control.update_control(self._steer_kp * 0.5, self._steer_kp * 1.5)
|
||||
if self._lat_accel_factor > 0:
|
||||
self._steer_lat_accel_control.update_control(self._lat_accel_factor * 0.5, self._lat_accel_factor * 1.5)
|
||||
if self._steer_ratio > 0:
|
||||
self._steer_ratio_control.update_control(self._steer_ratio * 0.5, self._steer_ratio * 1.5)
|
||||
|
||||
def _update_metric(self):
|
||||
if self._is_metric:
|
||||
speed_labels = build_metric_speed_labels()
|
||||
distance_labels = build_metric_distance_labels()
|
||||
max_speed = 150
|
||||
max_distance = 5.0
|
||||
else:
|
||||
speed_labels = build_imperial_speed_labels()
|
||||
distance_labels = build_imperial_distance_labels()
|
||||
max_speed = 99
|
||||
max_distance = 15.0
|
||||
|
||||
self._minimum_lane_change_speed_control.update_control(0, max_speed, speed_labels)
|
||||
self._pause_aol_on_brake_control.update_control(0, max_speed, speed_labels)
|
||||
self._pause_lateral_speed_control.update_control(0, max_speed, speed_labels)
|
||||
self._lane_detection_width_control.update_control(0, max_distance, distance_labels)
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
|
||||
forcing_auto_tune = not self._has_auto_tune and self._params.get_bool("ForceAutoTune")
|
||||
forcing_auto_tune_off = self._has_auto_tune and self._params.get_bool("ForceAutoTuneOff")
|
||||
forcing_torque_controller = not self._is_angle_car and self._params.get_bool("ForceTorqueController")
|
||||
using_nnff = self._has_nnff_log and self._params.get_bool("LateralTune") and self._params.get_bool("NNFF")
|
||||
nudgeless_enabled = self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange")
|
||||
|
||||
if hasattr(self._aol_lkas_item, 'set_visible'):
|
||||
self._aol_lkas_item.set_visible(self._lkas_allowed_for_aol)
|
||||
|
||||
if hasattr(self._force_auto_tune_item, 'set_visible'):
|
||||
visible = not self._has_auto_tune and not self._is_angle_car
|
||||
visible = visible and (self._is_torque_car or forcing_torque_controller or using_nnff)
|
||||
self._force_auto_tune_item.set_visible(visible)
|
||||
|
||||
if hasattr(self._force_auto_tune_off_item, 'set_visible'):
|
||||
self._force_auto_tune_off_item.set_visible(self._has_auto_tune)
|
||||
|
||||
if hasattr(self._force_torque_controller_item, 'set_visible'):
|
||||
visible = not self._is_angle_car and not self._is_torque_car
|
||||
self._force_torque_controller_item.set_visible(visible)
|
||||
|
||||
if hasattr(self._lane_change_time_control, 'set_visible'):
|
||||
self._lane_change_time_control.set_visible(nudgeless_enabled)
|
||||
|
||||
if hasattr(self._lane_detection_width_control, 'set_visible'):
|
||||
self._lane_detection_width_control.set_visible(nudgeless_enabled)
|
||||
|
||||
if hasattr(self._nnff_item, 'set_visible'):
|
||||
visible = self._has_nnff_log and not self._is_angle_car
|
||||
self._nnff_item.set_visible(visible)
|
||||
|
||||
if hasattr(self._nnff_lite_item, 'set_visible'):
|
||||
visible = not using_nnff and not self._is_angle_car
|
||||
self._nnff_lite_item.set_visible(visible)
|
||||
|
||||
if hasattr(self._steer_delay_control, 'set_visible'):
|
||||
self._steer_delay_control.set_visible(self._steer_actuator_delay != 0)
|
||||
|
||||
if hasattr(self._steer_friction_control, 'set_visible'):
|
||||
visible = self._friction != 0
|
||||
visible = visible and (self._has_auto_tune if forcing_auto_tune_off else not forcing_auto_tune)
|
||||
visible = visible and (self._is_torque_car or forcing_torque_controller or using_nnff)
|
||||
visible = visible and not using_nnff
|
||||
self._steer_friction_control.set_visible(visible)
|
||||
|
||||
if hasattr(self._steer_kp_control, 'set_visible'):
|
||||
visible = self._steer_kp != 0
|
||||
visible = visible and (self._is_torque_car or forcing_torque_controller or using_nnff)
|
||||
visible = visible and not self._is_angle_car
|
||||
self._steer_kp_control.set_visible(visible)
|
||||
|
||||
if hasattr(self._steer_lat_accel_control, 'set_visible'):
|
||||
visible = self._lat_accel_factor != 0
|
||||
visible = visible and (self._has_auto_tune if forcing_auto_tune_off else not forcing_auto_tune)
|
||||
visible = visible and (self._is_torque_car or forcing_torque_controller or using_nnff)
|
||||
visible = visible and not using_nnff
|
||||
self._steer_lat_accel_control.set_visible(visible)
|
||||
|
||||
if hasattr(self._steer_ratio_control, 'set_visible'):
|
||||
visible = self._steer_ratio != 0
|
||||
visible = visible and (self._has_auto_tune if forcing_auto_tune_off else not forcing_auto_tune)
|
||||
self._steer_ratio_control.set_visible(visible)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._is_metric = self._params.get_bool("IsMetric")
|
||||
self._update_car_params()
|
||||
self._update_metric()
|
||||
self._update_toggles()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_panel == SubPanel.ADVANCED_LATERAL_TUNE:
|
||||
self._advanced_lateral_tune_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.AOL:
|
||||
self._aol_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.LANE_CHANGE:
|
||||
self._lane_change_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.LATERAL_TUNE:
|
||||
self._lateral_tune_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.QOL:
|
||||
self._qol_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,480 @@
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog, DialogResult
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ButtonAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
)
|
||||
|
||||
MAPS_FOLDER_PATH = Path("/data/media/0/osm/offline")
|
||||
|
||||
# US State maps by region
|
||||
MIDWEST_MAP = {
|
||||
"IL": "Illinois", "IN": "Indiana", "IA": "Iowa",
|
||||
"KS": "Kansas", "MI": "Michigan", "MN": "Minnesota",
|
||||
"MO": "Missouri", "NE": "Nebraska", "ND": "North Dakota",
|
||||
"OH": "Ohio", "SD": "South Dakota", "WI": "Wisconsin"
|
||||
}
|
||||
|
||||
NORTHEAST_MAP = {
|
||||
"CT": "Connecticut", "ME": "Maine", "MA": "Massachusetts",
|
||||
"NH": "New Hampshire", "NJ": "New Jersey", "NY": "New York",
|
||||
"PA": "Pennsylvania", "RI": "Rhode Island", "VT": "Vermont"
|
||||
}
|
||||
|
||||
SOUTH_MAP = {
|
||||
"AL": "Alabama", "AR": "Arkansas", "DE": "Delaware",
|
||||
"DC": "District of Columbia", "FL": "Florida", "GA": "Georgia",
|
||||
"KY": "Kentucky", "LA": "Louisiana", "MD": "Maryland",
|
||||
"MS": "Mississippi", "NC": "North Carolina", "OK": "Oklahoma",
|
||||
"SC": "South Carolina", "TN": "Tennessee", "TX": "Texas",
|
||||
"VA": "Virginia", "WV": "West Virginia"
|
||||
}
|
||||
|
||||
WEST_MAP = {
|
||||
"AK": "Alaska", "AZ": "Arizona", "CA": "California",
|
||||
"CO": "Colorado", "HI": "Hawaii", "ID": "Idaho",
|
||||
"MT": "Montana", "NV": "Nevada", "NM": "New Mexico",
|
||||
"OR": "Oregon", "UT": "Utah", "WA": "Washington",
|
||||
"WY": "Wyoming"
|
||||
}
|
||||
|
||||
TERRITORIES_MAP = {
|
||||
"AS": "American Samoa", "GU": "Guam", "MP": "Northern Mariana Islands",
|
||||
"PR": "Puerto Rico", "VI": "Virgin Islands"
|
||||
}
|
||||
|
||||
# World country maps by continent
|
||||
AFRICA_MAP = {
|
||||
"DZ": "Algeria", "AO": "Angola", "BJ": "Benin",
|
||||
"BW": "Botswana", "BF": "Burkina Faso", "BI": "Burundi",
|
||||
"CM": "Cameroon", "CF": "Central African Republic", "TD": "Chad",
|
||||
"KM": "Comoros", "CG": "Congo (Brazzaville)", "CD": "Congo (Kinshasa)",
|
||||
"DJ": "Djibouti", "EG": "Egypt", "GQ": "Equatorial Guinea",
|
||||
"ER": "Eritrea", "ET": "Ethiopia", "GA": "Gabon",
|
||||
"GM": "Gambia", "GH": "Ghana", "GN": "Guinea",
|
||||
"GW": "Guinea-Bissau", "CI": "Ivory Coast", "KE": "Kenya",
|
||||
"LS": "Lesotho", "LR": "Liberia", "LY": "Libya",
|
||||
"MG": "Madagascar", "MW": "Malawi", "ML": "Mali",
|
||||
"MR": "Mauritania", "MA": "Morocco", "MZ": "Mozambique",
|
||||
"NA": "Namibia", "NE": "Niger", "NG": "Nigeria",
|
||||
"RW": "Rwanda", "SN": "Senegal", "SL": "Sierra Leone",
|
||||
"SO": "Somalia", "ZA": "South Africa", "SS": "South Sudan",
|
||||
"SD": "Sudan", "SZ": "Swaziland", "TZ": "Tanzania",
|
||||
"TG": "Togo", "TN": "Tunisia", "UG": "Uganda",
|
||||
"ZM": "Zambia", "ZW": "Zimbabwe"
|
||||
}
|
||||
|
||||
ANTARCTICA_MAP = {"AQ": "Antarctica"}
|
||||
|
||||
ASIA_MAP = {
|
||||
"AF": "Afghanistan", "AM": "Armenia", "AZ": "Azerbaijan",
|
||||
"BH": "Bahrain", "BD": "Bangladesh", "BT": "Bhutan",
|
||||
"BN": "Brunei", "KH": "Cambodia", "CN": "China",
|
||||
"CY": "Cyprus", "TL": "East Timor", "HK": "Hong Kong",
|
||||
"IN": "India", "ID": "Indonesia", "IR": "Iran",
|
||||
"IQ": "Iraq", "IL": "Israel", "JP": "Japan",
|
||||
"JO": "Jordan", "KZ": "Kazakhstan", "KW": "Kuwait",
|
||||
"KG": "Kyrgyzstan", "LA": "Laos", "LB": "Lebanon",
|
||||
"MY": "Malaysia", "MV": "Maldives", "MO": "Macao",
|
||||
"MN": "Mongolia", "MM": "Myanmar", "NP": "Nepal",
|
||||
"KP": "North Korea", "OM": "Oman", "PK": "Pakistan",
|
||||
"PS": "Palestine", "PH": "Philippines", "QA": "Qatar",
|
||||
"RU": "Russia", "SA": "Saudi Arabia", "SG": "Singapore",
|
||||
"KR": "South Korea", "LK": "Sri Lanka", "SY": "Syria",
|
||||
"TW": "Taiwan", "TJ": "Tajikistan", "TH": "Thailand",
|
||||
"TR": "Turkey", "TM": "Turkmenistan", "AE": "United Arab Emirates",
|
||||
"UZ": "Uzbekistan", "VN": "Vietnam", "YE": "Yemen"
|
||||
}
|
||||
|
||||
EUROPE_MAP = {
|
||||
"AL": "Albania", "AT": "Austria", "BY": "Belarus",
|
||||
"BE": "Belgium", "BA": "Bosnia and Herzegovina", "BG": "Bulgaria",
|
||||
"HR": "Croatia", "CZ": "Czech Republic", "DK": "Denmark",
|
||||
"EE": "Estonia", "FI": "Finland", "FR": "France",
|
||||
"GE": "Georgia", "DE": "Germany", "GR": "Greece",
|
||||
"HU": "Hungary", "IS": "Iceland", "IE": "Ireland",
|
||||
"IT": "Italy", "KZ": "Kazakhstan", "LV": "Latvia",
|
||||
"LT": "Lithuania", "LU": "Luxembourg", "MK": "Macedonia",
|
||||
"MD": "Moldova", "ME": "Montenegro", "NL": "Netherlands",
|
||||
"NO": "Norway", "PL": "Poland", "PT": "Portugal",
|
||||
"RO": "Romania", "RS": "Serbia", "SK": "Slovakia",
|
||||
"SI": "Slovenia", "ES": "Spain", "SE": "Sweden",
|
||||
"CH": "Switzerland", "TR": "Turkey", "UA": "Ukraine",
|
||||
"GB": "United Kingdom"
|
||||
}
|
||||
|
||||
NORTH_AMERICA_MAP = {
|
||||
"BS": "Bahamas", "BZ": "Belize", "CA": "Canada",
|
||||
"CR": "Costa Rica", "CU": "Cuba", "DO": "Dominican Republic",
|
||||
"SV": "El Salvador", "GL": "Greenland", "GD": "Grenada",
|
||||
"GT": "Guatemala", "HT": "Haiti", "HN": "Honduras",
|
||||
"JM": "Jamaica", "MX": "Mexico", "NI": "Nicaragua",
|
||||
"PA": "Panama", "TT": "Trinidad and Tobago", "US": "United States"
|
||||
}
|
||||
|
||||
OCEANIA_MAP = {
|
||||
"AU": "Australia", "FJ": "Fiji", "TF": "French Southern Territories",
|
||||
"NC": "New Caledonia", "NZ": "New Zealand", "PG": "Papua New Guinea",
|
||||
"SB": "Solomon Islands", "VU": "Vanuatu"
|
||||
}
|
||||
|
||||
SOUTH_AMERICA_MAP = {
|
||||
"AR": "Argentina", "BO": "Bolivia", "BR": "Brazil",
|
||||
"CL": "Chile", "CO": "Colombia", "EC": "Ecuador",
|
||||
"FK": "Falkland Islands", "GY": "Guyana", "PY": "Paraguay",
|
||||
"PE": "Peru", "SR": "Suriname", "UY": "Uruguay",
|
||||
"VE": "Venezuela"
|
||||
}
|
||||
|
||||
SCHEDULE_OPTIONS = ["Manually", "Weekly", "Monthly"]
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
COUNTRIES = 1
|
||||
STATES = 2
|
||||
|
||||
|
||||
def calculate_directory_size(directory: Path) -> str:
|
||||
"""Calculate directory size and return formatted string."""
|
||||
MB = 1024.0 * 1024.0
|
||||
GB = 1024.0 * MB
|
||||
|
||||
if not directory.exists():
|
||||
return "0 MB"
|
||||
|
||||
total_size = 0
|
||||
for file in directory.rglob("*"):
|
||||
if file.is_file():
|
||||
total_size += file.stat().st_size
|
||||
|
||||
if total_size >= GB:
|
||||
return f"{total_size / GB:.2f} GB"
|
||||
return f"{total_size / MB:.2f} MB"
|
||||
|
||||
|
||||
def day_suffix(day: int) -> str:
|
||||
"""Get ordinal suffix for day."""
|
||||
if day % 10 == 1 and day != 11:
|
||||
return "st"
|
||||
if day % 10 == 2 and day != 12:
|
||||
return "nd"
|
||||
if day % 10 == 3 and day != 13:
|
||||
return "rd"
|
||||
return "th"
|
||||
|
||||
|
||||
def format_current_date() -> str:
|
||||
"""Format current date as 'Month Day(suffix), Year'."""
|
||||
now = datetime.now()
|
||||
return now.strftime(f"%B {now.day}{day_suffix(now.day)}, %Y")
|
||||
|
||||
|
||||
def format_elapsed_time(elapsed_ms: float) -> str:
|
||||
"""Format elapsed time in milliseconds to readable string."""
|
||||
total_seconds = int(elapsed_ms / 1000)
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
|
||||
parts = []
|
||||
if hours > 0:
|
||||
parts.append(f"{hours} {'hour' if hours == 1 else 'hours'}")
|
||||
if minutes > 0:
|
||||
parts.append(f"{minutes} {'minute' if minutes == 1 else 'minutes'}")
|
||||
parts.append(f"{seconds} {'second' if seconds == 1 else 'seconds'}")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
class FrogPilotMapsPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._toggles = {}
|
||||
|
||||
# State tracking
|
||||
self._cancelling_download = False
|
||||
self._has_maps_selected = False
|
||||
self._online = False
|
||||
self._parked = True
|
||||
self._started = False
|
||||
|
||||
# Download tracking
|
||||
self._download_start_time = None
|
||||
self._elapsed_time_ms = 0
|
||||
|
||||
# Pending dialog action tracking
|
||||
self._pending_action = None
|
||||
self._pending_data = {}
|
||||
|
||||
self._build_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_panel(self):
|
||||
# Preferred Schedule - ButtonControl for schedule options
|
||||
self._preferred_schedule_control = FrogPilotButtonsControl(
|
||||
"Automatically Update Maps",
|
||||
"<b>How often maps update</b> from \"OpenStreetMap (OSM)\" with the latest speed limit information. Weekly updates run every Sunday; monthly updates run on the 1st.",
|
||||
"",
|
||||
button_texts=SCHEDULE_OPTIONS,
|
||||
)
|
||||
self._preferred_schedule_control.set_click_callback(self._on_preferred_schedule_click)
|
||||
self._update_schedule_button()
|
||||
|
||||
# Download Maps Button
|
||||
self._download_maps_control = FrogPilotButtonsControl(
|
||||
"Download Maps",
|
||||
"<b>Manually update your selected map sources</b> so \"Speed Limit Controller\" has the latest speed limit information.",
|
||||
"",
|
||||
button_texts=["DOWNLOAD"],
|
||||
)
|
||||
self._download_maps_control.set_click_callback(self._on_download_maps_click)
|
||||
|
||||
# Last Updated Label
|
||||
last_update = self._params.get("LastMapsUpdate", encoding="utf-8") or "Never"
|
||||
self._last_updated_item = ListItem(
|
||||
title="Last Updated",
|
||||
action_item=TextAction(lambda: self._params.get("LastMapsUpdate", encoding="utf-8") or "Never", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
# Select Maps - Countries/States
|
||||
self._select_maps_control = FrogPilotButtonsControl(
|
||||
"Map Sources",
|
||||
"<b>Select the countries or U.S. states to use with \"Speed Limit Controller\".</b>",
|
||||
"",
|
||||
button_texts=["COUNTRIES", "STATES"],
|
||||
)
|
||||
self._select_maps_control.set_click_callback(self._on_select_maps_click)
|
||||
|
||||
# Progress labels
|
||||
self._download_status_item = ListItem(
|
||||
title="Progress",
|
||||
action_item=TextAction(lambda: self._get_download_status(), color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
self._download_time_elapsed_item = ListItem(
|
||||
title="Time Elapsed",
|
||||
action_item=TextAction(lambda: self._get_time_elapsed(), color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
self._download_eta_item = ListItem(
|
||||
title="Time Remaining",
|
||||
action_item=TextAction(lambda: self._get_download_eta(), color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
# Remove Maps Button
|
||||
self._remove_maps_control = FrogPilotButtonsControl(
|
||||
"Remove Maps",
|
||||
"<b>Delete downloaded map data</b> to free up storage space.",
|
||||
"",
|
||||
button_texts=["REMOVE"],
|
||||
)
|
||||
self._remove_maps_control.set_click_callback(self._on_remove_maps_click)
|
||||
|
||||
# Storage Used Label
|
||||
self._maps_size_item = ListItem(
|
||||
title="Storage Used",
|
||||
action_item=TextAction(lambda: calculate_directory_size(MAPS_FOLDER_PATH), color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
main_items = [
|
||||
self._preferred_schedule_control,
|
||||
self._download_maps_control,
|
||||
self._last_updated_item,
|
||||
self._select_maps_control,
|
||||
self._download_status_item,
|
||||
self._download_time_elapsed_item,
|
||||
self._download_eta_item,
|
||||
self._remove_maps_control,
|
||||
self._maps_size_item,
|
||||
]
|
||||
|
||||
# Initially hide download progress items
|
||||
if hasattr(self._download_status_item, "set_visible"):
|
||||
self._download_status_item.set_visible(False)
|
||||
self._download_time_elapsed_item.set_visible(False)
|
||||
self._download_eta_item.set_visible(False)
|
||||
|
||||
self._toggles["PreferredSchedule"] = self._preferred_schedule_control
|
||||
self._toggles["DownloadMaps"] = self._download_maps_control
|
||||
self._toggles["LastUpdated"] = self._last_updated_item
|
||||
self._toggles["SelectMaps"] = self._select_maps_control
|
||||
self._toggles["DownloadStatus"] = self._download_status_item
|
||||
self._toggles["DownloadTimeElapsed"] = self._download_time_elapsed_item
|
||||
self._toggles["DownloadETA"] = self._download_eta_item
|
||||
self._toggles["RemoveMaps"] = self._remove_maps_control
|
||||
self._toggles["MapsSize"] = self._maps_size_item
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _get_download_status(self) -> str:
|
||||
"""Get current download status."""
|
||||
return "Calculating..."
|
||||
|
||||
def _get_time_elapsed(self) -> str:
|
||||
"""Get formatted elapsed time."""
|
||||
if self._elapsed_time_ms > 0:
|
||||
return format_elapsed_time(self._elapsed_time_ms)
|
||||
return "Calculating..."
|
||||
|
||||
def _get_download_eta(self) -> str:
|
||||
"""Get estimated time remaining."""
|
||||
return "Calculating..."
|
||||
|
||||
def _update_schedule_button(self):
|
||||
"""Update schedule button to show current selection."""
|
||||
schedule_index = self._params.get_int("PreferredSchedule") or 0
|
||||
self._preferred_schedule_control.set_checked_button(schedule_index)
|
||||
|
||||
def _on_preferred_schedule_click(self, button_id: int):
|
||||
self._params.put_int("PreferredSchedule", button_id)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_download_maps_click(self, button_id: int):
|
||||
# Check if we're cancelling
|
||||
if self._params_memory.get_bool("DownloadMaps"):
|
||||
self._pending_action = "cancel_download"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Cancel the download?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
else:
|
||||
self._start_download()
|
||||
|
||||
def _start_download(self):
|
||||
"""Start the map download."""
|
||||
self._download_start_time = datetime.now()
|
||||
self._elapsed_time_ms = 0
|
||||
|
||||
# Show progress items
|
||||
if hasattr(self._download_status_item, "set_visible"):
|
||||
self._download_status_item.set_visible(True)
|
||||
self._download_time_elapsed_item.set_visible(True)
|
||||
self._download_eta_item.set_visible(True)
|
||||
|
||||
# Hide last updated and remove button
|
||||
if hasattr(self._last_updated_item, "set_visible"):
|
||||
self._last_updated_item.set_visible(False)
|
||||
if hasattr(self._remove_maps_control, "set_visible"):
|
||||
self._remove_maps_control.set_visible(False)
|
||||
|
||||
# Change button text to CANCEL
|
||||
self._download_maps_control.set_text(0, "CANCEL")
|
||||
|
||||
# Trigger download
|
||||
self._params_memory.put_bool("DownloadMaps", True)
|
||||
|
||||
def _cancel_download(self):
|
||||
"""Cancel the current download."""
|
||||
self._cancelling_download = True
|
||||
self._download_maps_control.set_enabled(False)
|
||||
|
||||
self._params_memory.put_bool("CancelDownloadMaps", True)
|
||||
self._params_memory.remove("DownloadMaps")
|
||||
|
||||
def reset():
|
||||
self._cancelling_download = False
|
||||
self._download_maps_control.set_enabled(True)
|
||||
self._download_maps_control.set_text(0, "DOWNLOAD")
|
||||
|
||||
if hasattr(self._download_status_item, "set_visible"):
|
||||
self._download_status_item.set_visible(False)
|
||||
self._download_time_elapsed_item.set_visible(False)
|
||||
self._download_eta_item.set_visible(False)
|
||||
|
||||
if hasattr(self._last_updated_item, "set_visible"):
|
||||
self._last_updated_item.set_visible(True)
|
||||
if hasattr(self._remove_maps_control, "set_visible"):
|
||||
self._remove_maps_control.set_visible(MAPS_FOLDER_PATH.exists())
|
||||
|
||||
threading.Timer(2.5, reset).start()
|
||||
|
||||
def _on_select_maps_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
self._current_panel = SubPanel.COUNTRIES
|
||||
else:
|
||||
self._current_panel = SubPanel.STATES
|
||||
|
||||
def _on_remove_maps_click(self, button_id: int):
|
||||
self._pending_action = "remove_maps"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Delete all downloaded maps?",
|
||||
"Delete",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def handle_dialog_result(self, result: DialogResult, selection: str = ""):
|
||||
"""Handle dialog results for pending actions."""
|
||||
action = self._pending_action
|
||||
self._pending_action = None
|
||||
|
||||
if action == "cancel_download":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._cancel_download()
|
||||
|
||||
elif action == "remove_maps":
|
||||
if result == DialogResult.CONFIRM:
|
||||
def remove_thread():
|
||||
if MAPS_FOLDER_PATH.exists():
|
||||
shutil.rmtree(MAPS_FOLDER_PATH, ignore_errors=True)
|
||||
threading.Thread(target=remove_thread, daemon=True).start()
|
||||
|
||||
def _update_toggles(self):
|
||||
self._has_maps_selected = bool(self._params.get("MapsSelected", encoding="utf-8"))
|
||||
|
||||
# Remove maps button only visible if maps folder exists
|
||||
if hasattr(self._remove_maps_control, "set_visible"):
|
||||
self._remove_maps_control.set_visible(MAPS_FOLDER_PATH.exists())
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._has_maps_selected = bool(self._params.get("MapsSelected", encoding="utf-8"))
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._update_toggles()
|
||||
self._update_schedule_button()
|
||||
self._started = ui_state.started
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
self._started = ui_state.started
|
||||
self._parked = not self._started
|
||||
|
||||
# Update download button enabled state
|
||||
download_active = self._params_memory.get_bool("DownloadMaps")
|
||||
self._download_maps_control.set_enabled(
|
||||
not self._cancelling_download and self._has_maps_selected and self._online and self._parked
|
||||
)
|
||||
|
||||
if self._current_panel == SubPanel.COUNTRIES:
|
||||
# Would render countries selection panel
|
||||
self._main_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.STATES:
|
||||
# Would render states selection panel
|
||||
self._main_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,770 @@
|
||||
import json
|
||||
import threading
|
||||
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import clean_model_name
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
FrogPilotConfirmationDialog,
|
||||
)
|
||||
|
||||
MODEL_DIR = Path("/data/models/")
|
||||
|
||||
TINYGRAD_SUFFIXES = [
|
||||
"_driving_policy_metadata.pkl",
|
||||
"_driving_policy_tinygrad.pkl",
|
||||
"_driving_vision_metadata.pkl",
|
||||
"_driving_vision_tinygrad.pkl",
|
||||
]
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
MODEL_LABELS = 1
|
||||
|
||||
|
||||
def has_all_tinygrad_files(model_key: str) -> bool:
|
||||
"""Check if a model has all required tinygrad files."""
|
||||
for suffix in TINYGRAD_SUFFIXES:
|
||||
if not (MODEL_DIR / f"{model_key}{suffix}").exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class FrogPilotModelPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# State tracking
|
||||
self._all_models_downloaded = False
|
||||
self._all_models_downloading = False
|
||||
self._cancelling_download = False
|
||||
self._current_model = ""
|
||||
self._default_model = ""
|
||||
self._finalizing_download = False
|
||||
self._model_downloading = False
|
||||
self._no_models_downloaded = False
|
||||
self._online = False
|
||||
self._parked = True
|
||||
self._started = False
|
||||
self._tinygrad_update = False
|
||||
self._updating_tinygrad = False
|
||||
|
||||
# Model mappings
|
||||
self._available_model_names: list[str] = []
|
||||
self._model_file_to_name: dict[str, str] = {}
|
||||
self._model_file_to_name_processed: dict[str, str] = {}
|
||||
|
||||
# Get default model
|
||||
default_model_bytes = self._params.get_key_default_value("DrivingModel")
|
||||
self._default_model = default_model_bytes.decode() if default_model_bytes else ""
|
||||
|
||||
self._build_main_panel()
|
||||
self._build_model_labels_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_main_panel(self):
|
||||
self._auto_download_item = ListItem(
|
||||
title="Automatically Download New Models",
|
||||
description="<b>Automatically download new driving models</b> as they become available.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("AutomaticallyDownloadModels"),
|
||||
callback=lambda state: self._simple_toggle("AutomaticallyDownloadModels", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._delete_model_control = FrogPilotButtonsControl(
|
||||
"Delete Driving Models",
|
||||
"<b>Delete downloaded driving models</b> to free up storage space.",
|
||||
"",
|
||||
button_texts=["DELETE", "DELETE ALL"],
|
||||
)
|
||||
self._delete_model_control.set_click_callback(self._on_delete_model_click)
|
||||
|
||||
self._download_model_control = FrogPilotButtonsControl(
|
||||
"Download Driving Models",
|
||||
"<b>Manually download driving models</b> to the device.",
|
||||
"",
|
||||
button_texts=["DOWNLOAD", "DOWNLOAD ALL"],
|
||||
)
|
||||
self._download_model_control.set_click_callback(self._on_download_model_click)
|
||||
|
||||
self._model_randomizer_item = ListItem(
|
||||
title="Model Randomizer",
|
||||
description="<b>Select a random driving model each drive</b> and use feedback prompts at the end of the drive to help find the model that best suits you!",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("ModelRandomizer"),
|
||||
callback=self._on_model_randomizer_toggle,
|
||||
),
|
||||
)
|
||||
|
||||
self._manage_blacklist_control = FrogPilotButtonsControl(
|
||||
"Manage Model Blacklist",
|
||||
"<b>Add or remove driving models from the \"Model Randomizer\" blacklist.</b>",
|
||||
"",
|
||||
button_texts=["ADD", "REMOVE", "REMOVE ALL"],
|
||||
)
|
||||
self._manage_blacklist_control.set_click_callback(self._on_manage_blacklist_click)
|
||||
|
||||
self._manage_scores_control = FrogPilotButtonsControl(
|
||||
"Manage Model Ratings",
|
||||
"<b>View or reset saved model ratings</b> used by the \"Model Randomizer\".",
|
||||
"",
|
||||
button_texts=["RESET", "VIEW"],
|
||||
)
|
||||
self._manage_scores_control.set_click_callback(self._on_manage_scores_click)
|
||||
|
||||
self._select_model_item = ListItem(
|
||||
title="Select Driving Model",
|
||||
description="<b>Choose which driving model openpilot uses.</b>",
|
||||
action_item=TextAction(lambda: self._get_current_model_display(), color=ITEM_TEXT_VALUE_COLOR),
|
||||
callback=self._on_select_model_click,
|
||||
)
|
||||
|
||||
self._update_tinygrad_control = FrogPilotButtonsControl(
|
||||
"Update Model Manager",
|
||||
"<b>Update the \"Model Manager\"</b> to support the latest models.",
|
||||
"",
|
||||
button_texts=["UPDATE"],
|
||||
)
|
||||
self._update_tinygrad_control.set_click_callback(self._on_update_tinygrad_click)
|
||||
|
||||
main_items = [
|
||||
self._auto_download_item,
|
||||
self._delete_model_control,
|
||||
self._download_model_control,
|
||||
self._model_randomizer_item,
|
||||
self._manage_blacklist_control,
|
||||
self._manage_scores_control,
|
||||
self._select_model_item,
|
||||
self._update_tinygrad_control,
|
||||
]
|
||||
|
||||
self._toggles["AutomaticallyDownloadModels"] = self._auto_download_item
|
||||
self._toggles["DeleteModel"] = self._delete_model_control
|
||||
self._toggles["DownloadModel"] = self._download_model_control
|
||||
self._toggles["ModelRandomizer"] = self._model_randomizer_item
|
||||
self._toggles["ManageBlacklistedModels"] = self._manage_blacklist_control
|
||||
self._toggles["ManageScores"] = self._manage_scores_control
|
||||
self._toggles["SelectModel"] = self._select_model_item
|
||||
self._toggles["UpdateTinygrad"] = self._update_tinygrad_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_model_labels_panel(self):
|
||||
self._model_labels_items: list[ListItem] = []
|
||||
self._model_labels_scroller = Scroller(self._model_labels_items, line_separator=True, spacing=0)
|
||||
|
||||
def _get_current_model_display(self) -> str:
|
||||
"""Get the display string for the current model."""
|
||||
display = self._current_model
|
||||
model_key = clean_model_name(self._params.get("DrivingModel", encoding="utf-8") or "")
|
||||
if model_key == self._default_model:
|
||||
display += " (Default)"
|
||||
return display
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_model_randomizer_toggle(self, state: bool):
|
||||
self._params.put_bool("ModelRandomizer", state)
|
||||
update_frogpilot_toggles()
|
||||
self._update_toggles()
|
||||
|
||||
if state and not self._all_models_downloaded:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"The \"Model Randomizer\" works only with downloaded models. Download all models now?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
|
||||
def _on_delete_model_click(self, button_id: int):
|
||||
deletable_models = self._get_deletable_models()
|
||||
|
||||
if not deletable_models:
|
||||
gui_app.set_modal_overlay(alert_dialog("No models available to delete."))
|
||||
return
|
||||
|
||||
if button_id == 0:
|
||||
# Delete single model
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Select a driving model to delete",
|
||||
deletable_models,
|
||||
))
|
||||
elif button_id == 1:
|
||||
# Delete all models
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to delete all of your downloaded driving models?",
|
||||
"Delete",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def _get_deletable_models(self) -> list[str]:
|
||||
"""Get list of models that can be deleted (excludes current and default)."""
|
||||
deletable = []
|
||||
|
||||
if not MODEL_DIR.exists():
|
||||
return deletable
|
||||
|
||||
for file in MODEL_DIR.iterdir():
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
base = file.stem
|
||||
for model_key in self._model_file_to_name_processed:
|
||||
if base.startswith(model_key):
|
||||
model_name = self._model_file_to_name_processed[model_key]
|
||||
if model_name not in deletable:
|
||||
deletable.append(model_name)
|
||||
break
|
||||
|
||||
# Remove current model and default model from deletable list
|
||||
current_clean = clean_model_name(self._current_model)
|
||||
if current_clean in deletable:
|
||||
deletable.remove(current_clean)
|
||||
|
||||
default_name = self._model_file_to_name_processed.get(clean_model_name(self._default_model), "")
|
||||
if default_name in deletable:
|
||||
deletable.remove(default_name)
|
||||
|
||||
deletable.sort()
|
||||
return deletable
|
||||
|
||||
def _delete_model(self, model_name: str):
|
||||
"""Delete a specific model's files."""
|
||||
model_file = None
|
||||
for key, name in self._model_file_to_name_processed.items():
|
||||
if name == model_name:
|
||||
model_file = key
|
||||
break
|
||||
|
||||
if not model_file or not MODEL_DIR.exists():
|
||||
return
|
||||
|
||||
for file in MODEL_DIR.iterdir():
|
||||
if file.is_file() and file.stem.startswith(model_file):
|
||||
file.unlink()
|
||||
|
||||
self._all_models_downloaded = False
|
||||
self._update_deletable_state()
|
||||
|
||||
def _delete_all_models(self):
|
||||
"""Delete all deletable models."""
|
||||
deletable = self._get_deletable_models()
|
||||
|
||||
if not MODEL_DIR.exists():
|
||||
return
|
||||
|
||||
for file in MODEL_DIR.iterdir():
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
base = file.stem
|
||||
for model_key in self._model_file_to_name_processed:
|
||||
model_name = self._model_file_to_name_processed[model_key]
|
||||
if model_name in deletable and base.startswith(model_key):
|
||||
file.unlink()
|
||||
break
|
||||
|
||||
self._all_models_downloaded = False
|
||||
self._no_models_downloaded = True
|
||||
self._update_deletable_state()
|
||||
|
||||
def _update_deletable_state(self):
|
||||
"""Update the enabled state of delete buttons."""
|
||||
deletable = self._get_deletable_models()
|
||||
self._no_models_downloaded = len(deletable) == 0
|
||||
can_delete = not (self._all_models_downloading or self._model_downloading or self._no_models_downloaded)
|
||||
self._delete_model_control.set_enabled(can_delete)
|
||||
|
||||
def _on_download_model_click(self, button_id: int):
|
||||
if self._tinygrad_update:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Tinygrad is out of date and must be updated before you can download new models. Update now?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
return
|
||||
|
||||
if button_id == 0:
|
||||
# Download single model or cancel
|
||||
if self._model_downloading:
|
||||
self._params_memory.put_bool("CancelModelDownload", True)
|
||||
self._cancelling_download = True
|
||||
else:
|
||||
downloadable = self._get_downloadable_models()
|
||||
if not downloadable:
|
||||
gui_app.set_modal_overlay(alert_dialog("All models are already downloaded."))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Select a driving model to download",
|
||||
downloadable,
|
||||
))
|
||||
elif button_id == 1:
|
||||
# Download all or cancel
|
||||
if self._all_models_downloading:
|
||||
self._params_memory.put_bool("CancelModelDownload", True)
|
||||
self._cancelling_download = True
|
||||
else:
|
||||
self._params_memory.put_bool("DownloadAllModels", True)
|
||||
self._params_memory.put("ModelDownloadProgress", "Downloading...")
|
||||
self._download_model_control.set_text(1, "CANCEL")
|
||||
self._download_model_control.set_visible_button(0, False)
|
||||
self._all_models_downloading = True
|
||||
|
||||
def _get_downloadable_models(self) -> list[str]:
|
||||
"""Get list of models that can be downloaded."""
|
||||
downloadable = list(self._available_model_names)
|
||||
|
||||
for model_key in self._model_file_to_name:
|
||||
model_name = self._model_file_to_name[model_key]
|
||||
if has_all_tinygrad_files(model_key):
|
||||
if model_name in downloadable:
|
||||
downloadable.remove(model_name)
|
||||
|
||||
downloadable.sort()
|
||||
return downloadable
|
||||
|
||||
def _start_model_download(self, model_name: str):
|
||||
"""Start downloading a specific model."""
|
||||
model_key = None
|
||||
for key, name in self._model_file_to_name.items():
|
||||
if name == model_name:
|
||||
model_key = key
|
||||
break
|
||||
|
||||
if model_key:
|
||||
self._params_memory.put("ModelToDownload", model_key)
|
||||
self._params_memory.put("ModelDownloadProgress", "Downloading...")
|
||||
self._download_model_control.set_text(0, "CANCEL")
|
||||
self._download_model_control.set_visible_button(1, False)
|
||||
self._model_downloading = True
|
||||
|
||||
def _on_manage_blacklist_click(self, button_id: int):
|
||||
blacklisted_str = self._params.get("BlacklistedModels", encoding="utf-8") or ""
|
||||
blacklisted = [m for m in blacklisted_str.split(",") if m]
|
||||
|
||||
if button_id == 0:
|
||||
# Add to blacklist
|
||||
blacklistable = []
|
||||
for model_key in self._model_file_to_name_processed:
|
||||
if model_key not in blacklisted:
|
||||
blacklistable.append(self._model_file_to_name_processed[model_key])
|
||||
|
||||
if len(blacklistable) <= 1:
|
||||
remaining = blacklistable[0] if blacklistable else "None"
|
||||
gui_app.set_modal_overlay(alert_dialog(
|
||||
f"There are no more driving models to blacklist. The only available model is \"{remaining}\"!"
|
||||
))
|
||||
return
|
||||
|
||||
blacklistable.sort()
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Select a driving model to add to the blacklist",
|
||||
blacklistable,
|
||||
))
|
||||
|
||||
elif button_id == 1:
|
||||
# Remove from blacklist
|
||||
whitelistable = []
|
||||
for model_key in blacklisted:
|
||||
model_name = self._model_file_to_name_processed.get(model_key, "")
|
||||
if model_name:
|
||||
whitelistable.append(model_name)
|
||||
|
||||
if not whitelistable:
|
||||
gui_app.set_modal_overlay(alert_dialog("No models are currently blacklisted."))
|
||||
return
|
||||
|
||||
whitelistable.sort()
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Select a driving model to remove from the blacklist",
|
||||
whitelistable,
|
||||
))
|
||||
|
||||
elif button_id == 2:
|
||||
# Remove all from blacklist
|
||||
if not blacklisted:
|
||||
gui_app.set_modal_overlay(alert_dialog("No models are currently blacklisted."))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to remove all of your blacklisted driving models?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
|
||||
def _add_to_blacklist(self, model_name: str):
|
||||
"""Add a model to the blacklist."""
|
||||
model_key = None
|
||||
for key, name in self._model_file_to_name_processed.items():
|
||||
if name == model_name:
|
||||
model_key = key
|
||||
break
|
||||
|
||||
if model_key:
|
||||
blacklisted_str = self._params.get("BlacklistedModels", encoding="utf-8") or ""
|
||||
blacklisted = [m for m in blacklisted_str.split(",") if m]
|
||||
if model_key not in blacklisted:
|
||||
blacklisted.append(model_key)
|
||||
self._params.put("BlacklistedModels", ",".join(blacklisted))
|
||||
|
||||
def _remove_from_blacklist(self, model_name: str):
|
||||
"""Remove a model from the blacklist."""
|
||||
model_key = None
|
||||
for key, name in self._model_file_to_name_processed.items():
|
||||
if name == model_name:
|
||||
model_key = key
|
||||
break
|
||||
|
||||
if model_key:
|
||||
blacklisted_str = self._params.get("BlacklistedModels", encoding="utf-8") or ""
|
||||
blacklisted = [m for m in blacklisted_str.split(",") if m]
|
||||
if model_key in blacklisted:
|
||||
blacklisted.remove(model_key)
|
||||
self._params.put("BlacklistedModels", ",".join(blacklisted))
|
||||
|
||||
def _clear_blacklist(self):
|
||||
"""Clear all models from blacklist."""
|
||||
self._params.remove("BlacklistedModels")
|
||||
|
||||
def _on_manage_scores_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
# Reset scores
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Reset all model drives and ratings? This clears your drive history and collected feedback!",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
elif button_id == 1:
|
||||
# View scores
|
||||
self._update_model_labels()
|
||||
self._current_panel = SubPanel.MODEL_LABELS
|
||||
|
||||
def _reset_model_scores(self):
|
||||
"""Reset all model drives and scores."""
|
||||
self._params.remove("ModelDrivesAndScores")
|
||||
|
||||
def _update_model_labels(self):
|
||||
"""Update the model labels panel with current ratings."""
|
||||
self._model_labels_items.clear()
|
||||
|
||||
scores_str = self._params.get("ModelDrivesAndScores", encoding="utf-8") or "{}"
|
||||
try:
|
||||
model_drives_and_scores = json.loads(scores_str)
|
||||
except json.JSONDecodeError:
|
||||
model_drives_and_scores = {}
|
||||
|
||||
for model_name in sorted(self._available_model_names):
|
||||
clean_name = clean_model_name(model_name)
|
||||
model_data = model_drives_and_scores.get(clean_name, {})
|
||||
|
||||
drives = model_data.get("Drives", 0)
|
||||
score = model_data.get("Score", 0)
|
||||
|
||||
if drives == 1:
|
||||
drives_display = f"{drives} Drive"
|
||||
elif drives > 0:
|
||||
drives_display = f"{drives} Drives"
|
||||
else:
|
||||
drives_display = "N/A"
|
||||
|
||||
if drives > 0:
|
||||
score_display = f"Score: {score}%"
|
||||
else:
|
||||
score_display = "N/A"
|
||||
|
||||
label_text = f"{score_display} ({drives_display})"
|
||||
|
||||
item = ListItem(
|
||||
title=clean_name,
|
||||
action_item=TextAction(label_text, color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
self._model_labels_items.append(item)
|
||||
|
||||
self._model_labels_scroller = Scroller(self._model_labels_items, line_separator=True, spacing=0)
|
||||
|
||||
def _on_select_model_click(self):
|
||||
selectable = []
|
||||
|
||||
for model_key in self._model_file_to_name:
|
||||
if model_key != clean_model_name(self._default_model) and has_all_tinygrad_files(model_key):
|
||||
selectable.append(self._model_file_to_name[model_key])
|
||||
|
||||
selectable.sort()
|
||||
|
||||
# Add default model at the beginning
|
||||
default_name = self._model_file_to_name.get(clean_model_name(self._default_model), "")
|
||||
if default_name:
|
||||
selectable.insert(0, f"{default_name} (Default)")
|
||||
|
||||
current_display = self._current_model
|
||||
model_key = clean_model_name(self._params.get("DrivingModel", encoding="utf-8") or "")
|
||||
if model_key == self._default_model:
|
||||
current_display += " (Default)"
|
||||
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Select a Model",
|
||||
selectable,
|
||||
current_display,
|
||||
))
|
||||
|
||||
def _select_model(self, model_name: str):
|
||||
"""Select a driving model."""
|
||||
model_name = model_name.replace(" (Default)", "")
|
||||
self._current_model = model_name
|
||||
|
||||
model_key = None
|
||||
for key, name in self._model_file_to_name.items():
|
||||
if name == model_name:
|
||||
model_key = key
|
||||
break
|
||||
|
||||
if model_key:
|
||||
self._params.put("DrivingModel", model_key)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
if self._started:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Reboot required to take effect.",
|
||||
"Reboot Now",
|
||||
"Reboot Later",
|
||||
))
|
||||
|
||||
self._update_deletable_state()
|
||||
|
||||
def _on_update_tinygrad_click(self, button_id: int):
|
||||
if self._updating_tinygrad:
|
||||
self._params_memory.put_bool("CancelModelDownload", True)
|
||||
self._update_tinygrad_control.set_enabled(False)
|
||||
self._cancelling_download = True
|
||||
else:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Updating Tinygrad will delete existing Tinygrad-based driving models and need to be re-downloaded. Proceed?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
|
||||
def _start_tinygrad_update(self):
|
||||
"""Start the tinygrad update process."""
|
||||
self._params_memory.put_bool("UpdateTinygrad", True)
|
||||
self._params_memory.put("ModelDownloadProgress", "Downloading...")
|
||||
self._update_tinygrad_control.set_text(0, "CANCEL")
|
||||
self._updating_tinygrad = True
|
||||
|
||||
def _translate_progress(self, progress: str) -> str:
|
||||
"""Translate download progress messages."""
|
||||
translations = {
|
||||
"Downloading...": "Downloading...",
|
||||
"Downloaded!": "Downloaded!",
|
||||
"All models downloaded!": "All models downloaded!",
|
||||
"Repository unavailable": "Repository unavailable",
|
||||
}
|
||||
|
||||
if progress in translations:
|
||||
return translations[progress]
|
||||
|
||||
progress_lower = progress.lower()
|
||||
if "cancelled" in progress_lower:
|
||||
return "Download cancelled..."
|
||||
if "failed" in progress_lower:
|
||||
return "Download failed..."
|
||||
if "offline" in progress_lower:
|
||||
return "GitHub and GitLab are offline..."
|
||||
|
||||
return progress
|
||||
|
||||
def _update_download_state(self):
|
||||
"""Update UI based on download progress."""
|
||||
if self._finalizing_download:
|
||||
return
|
||||
|
||||
progress = self._params_memory.get("ModelDownloadProgress", encoding="utf-8") or ""
|
||||
|
||||
if self._all_models_downloading or self._model_downloading:
|
||||
import re
|
||||
download_failed = bool(re.search(r"cancelled|exists|failed|missing|offline", progress, re.IGNORECASE))
|
||||
|
||||
translated = self._translate_progress(progress)
|
||||
|
||||
if progress in ("All models downloaded!", "Downloaded!") or download_failed:
|
||||
self._finalizing_download = True
|
||||
|
||||
def finalize():
|
||||
self._all_models_downloading = False
|
||||
self._cancelling_download = False
|
||||
self._finalizing_download = False
|
||||
self._model_downloading = False
|
||||
self._no_models_downloaded = False
|
||||
|
||||
# Update all models downloaded state
|
||||
downloadable = self._get_downloadable_models()
|
||||
self._all_models_downloaded = len(downloadable) == 0
|
||||
|
||||
self._params_memory.remove("ModelDownloadProgress")
|
||||
|
||||
self._download_model_control.set_enabled(True)
|
||||
self._download_model_control.set_text(0, "DOWNLOAD")
|
||||
self._download_model_control.set_text(1, "DOWNLOAD ALL")
|
||||
self._download_model_control.set_visible_button(0, True)
|
||||
self._download_model_control.set_visible_button(1, True)
|
||||
|
||||
threading.Timer(2.5, finalize).start()
|
||||
|
||||
if self._updating_tinygrad:
|
||||
import re
|
||||
download_failed = bool(re.search(r"cancelled|exists|failed|missing|offline", progress, re.IGNORECASE))
|
||||
|
||||
translated = self._translate_progress(progress)
|
||||
|
||||
if progress == "Updated!" or download_failed:
|
||||
self._finalizing_download = True
|
||||
|
||||
def finalize_tinygrad():
|
||||
check_progress = self._params_memory.get("ModelDownloadProgress", encoding="utf-8") or ""
|
||||
self._model_downloading = bool(check_progress)
|
||||
|
||||
if self._model_downloading:
|
||||
self._download_model_control.set_text(1, "CANCEL")
|
||||
self._download_model_control.set_visible_button(0, False)
|
||||
else:
|
||||
self._cancelling_download = False
|
||||
|
||||
self._tinygrad_update = self._params.get_bool("TinygradUpdateAvailable")
|
||||
self._finalizing_download = False
|
||||
self._updating_tinygrad = False
|
||||
|
||||
self._update_tinygrad_control.set_enabled(self._tinygrad_update)
|
||||
self._update_tinygrad_control.set_text(0, "UPDATE")
|
||||
|
||||
threading.Timer(2.5, finalize_tinygrad).start()
|
||||
|
||||
def _update_button_states(self):
|
||||
"""Update button enabled/visible states."""
|
||||
can_delete = not (self._all_models_downloading or self._model_downloading or self._no_models_downloaded)
|
||||
self._delete_model_control.set_enabled(can_delete)
|
||||
|
||||
# Download buttons
|
||||
self._download_model_control.set_text(0, "CANCEL" if self._model_downloading else "DOWNLOAD")
|
||||
self._download_model_control.set_text(1, "CANCEL" if self._all_models_downloading else "DOWNLOAD ALL")
|
||||
|
||||
can_download_single = (not self._all_models_downloaded and not self._all_models_downloading and
|
||||
not self._cancelling_download and not self._finalizing_download and
|
||||
not self._updating_tinygrad and self._online and self._parked)
|
||||
can_download_all = (not self._all_models_downloaded and not self._model_downloading and
|
||||
not self._cancelling_download and not self._finalizing_download and
|
||||
not self._updating_tinygrad and self._online and self._parked)
|
||||
|
||||
self._download_model_control.set_enabled_buttons(0, can_download_single)
|
||||
self._download_model_control.set_enabled_buttons(1, can_download_all)
|
||||
|
||||
self._download_model_control.set_visible_button(0, not self._all_models_downloading)
|
||||
self._download_model_control.set_visible_button(1, not self._model_downloading)
|
||||
|
||||
# Tinygrad update button
|
||||
can_update = (not self._model_downloading and not self._cancelling_download and
|
||||
not self._finalizing_download and self._online and self._parked and self._tinygrad_update)
|
||||
self._update_tinygrad_control.set_enabled(can_update)
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
model_randomizer = self._params.get_bool("ModelRandomizer")
|
||||
|
||||
# ManageBlacklistedModels and ManageScores only visible when ModelRandomizer enabled
|
||||
if hasattr(self._manage_blacklist_control, "set_visible"):
|
||||
self._manage_blacklist_control.set_visible(model_randomizer)
|
||||
if hasattr(self._manage_scores_control, "set_visible"):
|
||||
self._manage_scores_control.set_visible(model_randomizer)
|
||||
|
||||
# SelectModel only visible when ModelRandomizer disabled
|
||||
if hasattr(self._select_model_item, "set_visible"):
|
||||
self._select_model_item.set_visible(not model_randomizer)
|
||||
|
||||
def _load_model_data(self):
|
||||
"""Load available models and current state."""
|
||||
self._all_models_downloading = self._params_memory.get_bool("DownloadAllModels")
|
||||
progress = self._params_memory.get("ModelDownloadProgress", encoding="utf-8") or ""
|
||||
self._model_downloading = bool(progress)
|
||||
self._tinygrad_update = self._params.get_bool("TinygradUpdateAvailable")
|
||||
self._updating_tinygrad = self._params_memory.get_bool("UpdateTinygrad")
|
||||
|
||||
self._model_downloading = self._model_downloading and not self._updating_tinygrad
|
||||
|
||||
# Load available models
|
||||
available_models_str = self._params.get("AvailableModels", encoding="utf-8") or ""
|
||||
available_models = sorted([m for m in available_models_str.split(",") if m])
|
||||
|
||||
available_names_str = self._params.get("AvailableModelNames", encoding="utf-8") or ""
|
||||
self._available_model_names = sorted([m for m in available_names_str.split(",") if m])
|
||||
|
||||
# Build mappings
|
||||
self._model_file_to_name.clear()
|
||||
self._model_file_to_name_processed.clear()
|
||||
for i in range(min(len(available_models), len(self._available_model_names))):
|
||||
model_key = available_models[i]
|
||||
model_name = self._available_model_names[i]
|
||||
self._model_file_to_name[model_key] = model_name
|
||||
self._model_file_to_name_processed[model_key] = clean_model_name(model_name)
|
||||
|
||||
# Check downloadable models
|
||||
downloadable = self._get_downloadable_models()
|
||||
self._all_models_downloaded = len(downloadable) == 0
|
||||
|
||||
# Check deletable models
|
||||
self._update_deletable_state()
|
||||
|
||||
# Get current model
|
||||
model_key = clean_model_name(self._params.get("DrivingModel", encoding="utf-8") or "")
|
||||
if not has_all_tinygrad_files(model_key):
|
||||
model_key = self._default_model
|
||||
self._current_model = self._model_file_to_name.get(model_key, "")
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._load_model_data()
|
||||
self._update_toggles()
|
||||
self._started = ui_state.started
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
# Update online/parked state
|
||||
self._started = ui_state.started
|
||||
self._parked = not self._started # Simplified - in real impl check frogpilot_scene.parked
|
||||
|
||||
# Update download state
|
||||
self._update_download_state()
|
||||
self._update_button_states()
|
||||
|
||||
if self._current_panel == SubPanel.MODEL_LABELS:
|
||||
self._model_labels_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,371 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
from datetime import date, datetime
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog, DialogResult
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ButtonAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
FrogPilotButtonControl,
|
||||
)
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
INSTRUCTIONS = 1
|
||||
|
||||
|
||||
class FrogPilotNavigationPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# State tracking
|
||||
self._mapbox_public_key_set = False
|
||||
self._mapbox_secret_key_set = False
|
||||
self._online = False
|
||||
self._parked = True
|
||||
self._started = False
|
||||
self._updating_limits = False
|
||||
|
||||
# Pending dialog action tracking
|
||||
self._pending_action = None # "add_public_key", "remove_public_key", "add_secret_key", "remove_secret_key", "cancel_update", "start_update"
|
||||
self._pending_data = {}
|
||||
|
||||
# Keyboard for text input
|
||||
self._keyboard = Keyboard()
|
||||
|
||||
self._build_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_panel(self):
|
||||
# IP Label
|
||||
self._ip_label_item = ListItem(
|
||||
title="Manage Your Settings At",
|
||||
action_item=TextAction(lambda: self._get_ip_address(), color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
# Public Mapbox Key Control
|
||||
self._public_mapbox_control = FrogPilotButtonsControl(
|
||||
"Public Mapbox Key",
|
||||
"<b>Manage your Public Mapbox Key.</b>",
|
||||
"",
|
||||
button_texts=["ADD", "TEST"],
|
||||
)
|
||||
self._public_mapbox_control.set_click_callback(self._on_public_mapbox_click)
|
||||
|
||||
# Secret Mapbox Key Control
|
||||
self._secret_mapbox_control = FrogPilotButtonsControl(
|
||||
"Secret Mapbox Key",
|
||||
"<b>Manage your Secret Mapbox Key.</b>",
|
||||
"",
|
||||
button_texts=["ADD", "TEST"],
|
||||
)
|
||||
self._secret_mapbox_control.set_click_callback(self._on_secret_mapbox_click)
|
||||
|
||||
# Setup Button
|
||||
self._setup_button_item = ListItem(
|
||||
title="Mapbox Setup Instructions",
|
||||
description="<b>Instructions on how to set up Mapbox</b> for \"Primeless Navigation\".",
|
||||
action_item=ButtonAction(
|
||||
text="VIEW",
|
||||
callback=self._on_setup_click,
|
||||
),
|
||||
)
|
||||
|
||||
# Speed Limit Filler Control
|
||||
self._speed_limit_filler_control = FrogPilotButtonControl(
|
||||
"SpeedLimitFiller",
|
||||
"Speed Limit Filler",
|
||||
"<b>Automatically collect missing or incorrect speed limits while you drive</b> using speeds limits sourced from your dashboard (if supported), "
|
||||
"Mapbox, and \"Navigate on openpilot\".<br><br>"
|
||||
"When you're parked and connected to Wi-Fi, FrogPilot will automatically processes this data into a file "
|
||||
"to be used with the tool located at \"SpeedLimitFiller.frogpilot.com\".<br><br>"
|
||||
"You can download this file from \"The Pond\" in the \"Download Speed Limits\" menu.<br><br>"
|
||||
"Need a step-by-step guide? Visit <b>#speed-limit-filler</b> in the FrogPilot Discord!",
|
||||
"",
|
||||
button_texts=["CANCEL", "Manually Update Speed Limits"],
|
||||
)
|
||||
self._speed_limit_filler_control.set_button_click_callback(self._on_speed_limit_filler_click)
|
||||
self._speed_limit_filler_control.set_visible_button(0, False)
|
||||
|
||||
main_items = [
|
||||
self._ip_label_item,
|
||||
self._public_mapbox_control,
|
||||
self._secret_mapbox_control,
|
||||
self._setup_button_item,
|
||||
self._speed_limit_filler_control,
|
||||
]
|
||||
|
||||
self._toggles["IPLabel"] = self._ip_label_item
|
||||
self._toggles["PublicMapboxKey"] = self._public_mapbox_control
|
||||
self._toggles["SecretMapboxKey"] = self._secret_mapbox_control
|
||||
self._toggles["SetupButton"] = self._setup_button_item
|
||||
self._toggles["SpeedLimitFiller"] = self._speed_limit_filler_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _get_ip_address(self) -> str:
|
||||
"""Get current IP address for settings management."""
|
||||
# This would need to be wired up to the wifi module
|
||||
return "Offline..."
|
||||
|
||||
def _update_buttons(self):
|
||||
"""Update Mapbox key button states."""
|
||||
public_key = self._params.get("MapboxPublicKey", encoding="utf-8") or ""
|
||||
secret_key = self._params.get("MapboxSecretKey", encoding="utf-8") or ""
|
||||
|
||||
self._mapbox_public_key_set = public_key.startswith("pk")
|
||||
self._mapbox_secret_key_set = secret_key.startswith("sk")
|
||||
|
||||
self._public_mapbox_control.set_text(0, "REMOVE" if self._mapbox_public_key_set else "ADD")
|
||||
self._public_mapbox_control.set_visible_button(1, self._mapbox_public_key_set and self._online)
|
||||
|
||||
self._secret_mapbox_control.set_text(0, "REMOVE" if self._mapbox_secret_key_set else "ADD")
|
||||
self._secret_mapbox_control.set_visible_button(1, self._mapbox_secret_key_set and self._online)
|
||||
|
||||
def _on_public_mapbox_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
# ADD or REMOVE
|
||||
if self._mapbox_public_key_set:
|
||||
self._pending_action = "remove_public_key"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Remove your Public Mapbox Key?",
|
||||
"Remove",
|
||||
"Cancel",
|
||||
))
|
||||
else:
|
||||
self._pending_action = "add_public_key"
|
||||
self._keyboard.reset(min_text_size=80)
|
||||
self._keyboard.set_title("Enter your Public Mapbox Key")
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
elif button_id == 1:
|
||||
# TEST
|
||||
self._test_public_key()
|
||||
|
||||
def _on_secret_mapbox_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
# ADD or REMOVE
|
||||
if self._mapbox_secret_key_set:
|
||||
self._pending_action = "remove_secret_key"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Remove your Secret Mapbox Key?",
|
||||
"Remove",
|
||||
"Cancel",
|
||||
))
|
||||
else:
|
||||
self._pending_action = "add_secret_key"
|
||||
self._keyboard.reset(min_text_size=80)
|
||||
self._keyboard.set_title("Enter your Secret Mapbox Key")
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
elif button_id == 1:
|
||||
# TEST
|
||||
self._test_secret_key()
|
||||
|
||||
def _test_public_key(self):
|
||||
"""Test the public Mapbox key."""
|
||||
self._public_mapbox_control.set_value("Testing...")
|
||||
|
||||
# In a real implementation, this would make an HTTP request
|
||||
# For now, we'll just show a placeholder response
|
||||
def test_thread():
|
||||
time.sleep(1)
|
||||
self._public_mapbox_control.set_value("")
|
||||
# Would show result dialog here
|
||||
threading.Thread(target=test_thread, daemon=True).start()
|
||||
|
||||
def _test_secret_key(self):
|
||||
"""Test the secret Mapbox key."""
|
||||
self._secret_mapbox_control.set_value("Testing...")
|
||||
|
||||
def test_thread():
|
||||
time.sleep(1)
|
||||
self._secret_mapbox_control.set_value("")
|
||||
# Would show result dialog here
|
||||
threading.Thread(target=test_thread, daemon=True).start()
|
||||
|
||||
def _on_setup_click(self):
|
||||
self._current_panel = SubPanel.INSTRUCTIONS
|
||||
|
||||
def _on_speed_limit_filler_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
# CANCEL
|
||||
self._pending_action = "cancel_update"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Cancel the speed-limit update?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
elif button_id == 1:
|
||||
# Manually Update Speed Limits
|
||||
# Check request limits
|
||||
overpass_requests_str = self._params.get("OverpassRequests", encoding="utf-8") or "{}"
|
||||
try:
|
||||
overpass_requests = json.loads(overpass_requests_str)
|
||||
except json.JSONDecodeError:
|
||||
overpass_requests = {}
|
||||
|
||||
total_requests = overpass_requests.get("total_requests", 0)
|
||||
max_requests = overpass_requests.get("max_requests", 10000)
|
||||
saved_day = overpass_requests.get("day", date.today().day)
|
||||
|
||||
current_day = date.today().day
|
||||
|
||||
if saved_day != current_day:
|
||||
total_requests = 0
|
||||
|
||||
if total_requests >= max_requests:
|
||||
now = datetime.now()
|
||||
seconds_until_midnight = (24 * 3600) - (now.hour * 3600 + now.minute * 60 + now.second)
|
||||
hours = seconds_until_midnight // 3600
|
||||
minutes = (seconds_until_midnight % 3600) // 60
|
||||
|
||||
gui_app.set_modal_overlay(alert_dialog(
|
||||
f"You've hit today's request limit.\n\nIt will reset in {hours} hours and {minutes} minutes."
|
||||
))
|
||||
self._speed_limit_filler_control.clear_checked_buttons()
|
||||
return
|
||||
|
||||
self._speed_limit_filler_control.set_visible_button(0, True)
|
||||
self._speed_limit_filler_control.set_visible_button(1, False)
|
||||
|
||||
self._pending_action = "start_update"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"This process takes a while. It's recommended to start when you're done driving and connected to stable Wi-Fi. Continue?",
|
||||
"Continue",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def _on_keyboard_result(self, result: DialogResult):
|
||||
"""Callback for keyboard modal overlay."""
|
||||
self.handle_dialog_result(result, self._keyboard.text)
|
||||
|
||||
def handle_dialog_result(self, result: DialogResult, selection: str = ""):
|
||||
"""Handle dialog results for pending actions."""
|
||||
action = self._pending_action
|
||||
self._pending_action = None
|
||||
|
||||
if action == "add_public_key":
|
||||
if result == DialogResult.CONFIRM and selection:
|
||||
key = selection.strip()
|
||||
if not key.startswith("pk."):
|
||||
key = "pk." + key
|
||||
self._params.put("MapboxPublicKey", key)
|
||||
self._update_buttons()
|
||||
|
||||
elif action == "remove_public_key":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.remove("MapboxPublicKey")
|
||||
self._update_buttons()
|
||||
|
||||
elif action == "add_secret_key":
|
||||
if result == DialogResult.CONFIRM and selection:
|
||||
key = selection.strip()
|
||||
if not key.startswith("sk."):
|
||||
key = "sk." + key
|
||||
self._params.put("MapboxSecretKey", key)
|
||||
self._update_buttons()
|
||||
|
||||
elif action == "remove_secret_key":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.remove("MapboxSecretKey")
|
||||
self._update_buttons()
|
||||
|
||||
elif action == "cancel_update":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._updating_limits = False
|
||||
self._speed_limit_filler_control.set_enabled_button(0, False)
|
||||
self._speed_limit_filler_control.set_value("Cancelled...")
|
||||
self._params_memory.remove("UpdateSpeedLimits")
|
||||
|
||||
def reset():
|
||||
self._speed_limit_filler_control.clear_checked_buttons()
|
||||
self._speed_limit_filler_control.set_enabled_button(0, True)
|
||||
self._speed_limit_filler_control.set_value("")
|
||||
self._speed_limit_filler_control.set_visible_button(0, False)
|
||||
self._speed_limit_filler_control.set_visible_button(1, True)
|
||||
self._params_memory.remove("UpdateSpeedLimitsStatus")
|
||||
|
||||
threading.Timer(2.5, reset).start()
|
||||
|
||||
elif action == "start_update":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._updating_limits = True
|
||||
self._speed_limit_filler_control.set_value("Calculating...")
|
||||
self._params_memory.put("UpdateSpeedLimitsStatus", "Calculating...")
|
||||
self._params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
else:
|
||||
self._speed_limit_filler_control.set_visible_button(0, False)
|
||||
self._speed_limit_filler_control.set_visible_button(1, True)
|
||||
self._speed_limit_filler_control.clear_checked_buttons()
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
self._update_buttons()
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._update_toggles()
|
||||
self._started = ui_state.started
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
self._started = ui_state.started
|
||||
self._parked = not self._started
|
||||
|
||||
# Update speed limit filler state
|
||||
if self._updating_limits:
|
||||
status = self._params_memory.get("UpdateSpeedLimitsStatus", encoding="utf-8") or ""
|
||||
if status == "Completed!":
|
||||
self._updating_limits = False
|
||||
self._speed_limit_filler_control.set_value("Completed!")
|
||||
|
||||
def reset():
|
||||
self._speed_limit_filler_control.clear_checked_buttons()
|
||||
self._speed_limit_filler_control.set_value("")
|
||||
self._speed_limit_filler_control.set_visible_button(0, False)
|
||||
self._speed_limit_filler_control.set_visible_button(1, True)
|
||||
self._params_memory.remove("UpdateSpeedLimitsStatus")
|
||||
|
||||
threading.Timer(2.5, reset).start()
|
||||
else:
|
||||
self._speed_limit_filler_control.set_value(status)
|
||||
else:
|
||||
self._speed_limit_filler_control.set_enabled_button(1, self._online and self._parked)
|
||||
if not self._online:
|
||||
self._speed_limit_filler_control.set_value("Offline...")
|
||||
elif not self._parked:
|
||||
self._speed_limit_filler_control.set_value("Not parked")
|
||||
else:
|
||||
self._speed_limit_filler_control.set_value("")
|
||||
|
||||
if self._current_panel == SubPanel.INSTRUCTIONS:
|
||||
# Would render setup instructions image
|
||||
self._main_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,458 @@
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotManageControl,
|
||||
FrogPilotParamValueButtonControl,
|
||||
)
|
||||
|
||||
STOCK_SOUNDS_PATH = Path("/data/openpilot/selfdrive/assets/sounds")
|
||||
THEME_SOUNDS_PATH = ACTIVE_THEME_PATH / "sounds"
|
||||
|
||||
ALERT_VOLUME_CONTROL_KEYS = {
|
||||
"DisengageVolume",
|
||||
"EngageVolume",
|
||||
"PromptDistractedVolume",
|
||||
"PromptVolume",
|
||||
"RefuseVolume",
|
||||
"WarningImmediateVolume",
|
||||
"WarningSoftVolume",
|
||||
}
|
||||
|
||||
CUSTOM_ALERTS_KEYS = {
|
||||
"GoatScream",
|
||||
"GreenLightAlert",
|
||||
"LeadDepartingAlert",
|
||||
"LoudBlindspotAlert",
|
||||
"SpeedLimitChangedAlert",
|
||||
}
|
||||
|
||||
# Minimum volume for warning alerts (25%)
|
||||
WARNING_MIN_VOLUME = 25
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
ALERT_VOLUME_CONTROL = 1
|
||||
CUSTOM_ALERTS = 2
|
||||
|
||||
|
||||
def build_volume_labels() -> dict[int, str]:
|
||||
"""Build volume labels from 0-101 where 0=Muted, 101=Auto."""
|
||||
labels = {}
|
||||
for i in range(102):
|
||||
if i == 0:
|
||||
labels[i] = "Muted"
|
||||
elif i == 101:
|
||||
labels[i] = "Auto"
|
||||
else:
|
||||
labels[i] = f"{i}%"
|
||||
return labels
|
||||
|
||||
|
||||
def camel_to_snake(name: str) -> str:
|
||||
"""Convert CamelCase to snake_case."""
|
||||
return re.sub(r'([A-Z])', r'_\1', name).lower().lstrip('_')
|
||||
|
||||
|
||||
class FrogPilotSoundsPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._sound_player_process: subprocess.Popen | None = None
|
||||
self._started = False
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# Car capabilities (will be loaded from frogpilot_variables)
|
||||
self._has_bsm = False
|
||||
self._has_openpilot_longitudinal = False
|
||||
|
||||
self._build_main_panel()
|
||||
self._build_alert_volume_panel()
|
||||
self._build_custom_alerts_panel()
|
||||
|
||||
self._initialize_sound_player()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_main_panel(self):
|
||||
self._alert_volume_control = FrogPilotManageControl(
|
||||
"AlertVolumeControl",
|
||||
"Alert Volume Controller",
|
||||
"<b>Set how loud each type of openpilot alert is</b> to keep routine prompts from becoming distracting.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_mute.png",
|
||||
)
|
||||
self._alert_volume_control.set_manage_callback(self._open_alert_volume_panel)
|
||||
|
||||
self._custom_alerts_control = FrogPilotManageControl(
|
||||
"CustomAlerts",
|
||||
"FrogPilot Alerts",
|
||||
"<b>Optional FrogPilot alerts</b> that highlight driving events in a more noticeable way.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_green_light.png",
|
||||
)
|
||||
self._custom_alerts_control.set_manage_callback(self._open_custom_alerts_panel)
|
||||
|
||||
main_items = [
|
||||
self._alert_volume_control,
|
||||
self._custom_alerts_control,
|
||||
]
|
||||
|
||||
self._toggles["AlertVolumeControl"] = self._alert_volume_control
|
||||
self._toggles["CustomAlerts"] = self._custom_alerts_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_alert_volume_panel(self):
|
||||
volume_labels = build_volume_labels()
|
||||
|
||||
# Disengage Volume (0-101)
|
||||
self._disengage_volume_control = FrogPilotParamValueButtonControl(
|
||||
"DisengageVolume",
|
||||
"Disengage Volume",
|
||||
"<b>Set the volume for alerts when openpilot disengages.</b><br><br>Examples include: \"Cruise Fault: Restart the Car\", \"Parking Brake Engaged\", \"Pedal Pressed\".",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._disengage_volume_control.set_button_click_callback(lambda _: self._test_sound("DisengageVolume"))
|
||||
|
||||
# Engage Volume (0-101)
|
||||
self._engage_volume_control = FrogPilotParamValueButtonControl(
|
||||
"EngageVolume",
|
||||
"Engage Volume",
|
||||
"<b>Set the volume for the chime when openpilot engages</b>, such as after pressing the \"RESUME\" or \"SET\" steering wheel buttons.",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._engage_volume_control.set_button_click_callback(lambda _: self._test_sound("EngageVolume"))
|
||||
|
||||
# Prompt Volume (0-101)
|
||||
self._prompt_volume_control = FrogPilotParamValueButtonControl(
|
||||
"PromptVolume",
|
||||
"Prompt Volume",
|
||||
"<b>Set the volume for prompts that need attention.</b><br><br>Examples include: \"Car Detected in Blindspot\", \"Steering Temporarily Unavailable\", \"Turn Exceeds Steering Limit\".",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._prompt_volume_control.set_button_click_callback(lambda _: self._test_sound("PromptVolume"))
|
||||
|
||||
# Prompt Distracted Volume (0-101)
|
||||
self._prompt_distracted_volume_control = FrogPilotParamValueButtonControl(
|
||||
"PromptDistractedVolume",
|
||||
"Prompt Distracted Volume",
|
||||
"<b>Set the volume for prompts when openpilot detects driver distraction or unresponsiveness.</b><br><br>Examples include: \"Pay Attention\", \"Touch Steering Wheel\".",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._prompt_distracted_volume_control.set_button_click_callback(lambda _: self._test_sound("PromptDistractedVolume"))
|
||||
|
||||
# Refuse Volume (0-101)
|
||||
self._refuse_volume_control = FrogPilotParamValueButtonControl(
|
||||
"RefuseVolume",
|
||||
"Refuse Volume",
|
||||
"<b>Set the volume for alerts when openpilot refuses to engage.</b><br><br>Examples include: \"Brake Hold Active\", \"Door Open\", \"Seatbelt Unlatched\".",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._refuse_volume_control.set_button_click_callback(lambda _: self._test_sound("RefuseVolume"))
|
||||
|
||||
# Warning Soft Volume (25-101, minimum 25%)
|
||||
self._warning_soft_volume_control = FrogPilotParamValueButtonControl(
|
||||
"WarningSoftVolume",
|
||||
"Warning Soft Volume",
|
||||
"<b>Set the volume for softer warnings about potential risks.</b><br><br>Examples include: \"BRAKE! Risk of Collision\", \"Steering Temporarily Unavailable\".",
|
||||
"",
|
||||
min_value=WARNING_MIN_VOLUME,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._warning_soft_volume_control.set_button_click_callback(lambda _: self._test_sound("WarningSoftVolume"))
|
||||
|
||||
# Warning Immediate Volume (25-101, minimum 25%)
|
||||
self._warning_immediate_volume_control = FrogPilotParamValueButtonControl(
|
||||
"WarningImmediateVolume",
|
||||
"Warning Immediate Volume",
|
||||
"<b>Set the volume for the loudest warnings that require urgent attention.</b><br><br>Examples include: \"DISENGAGE IMMEDIATELY — Driver Distracted\", \"DISENGAGE IMMEDIATELY — Driver Unresponsive\".",
|
||||
"",
|
||||
min_value=WARNING_MIN_VOLUME,
|
||||
max_value=101,
|
||||
value_labels=volume_labels,
|
||||
fast_increase=True,
|
||||
button_texts=["Test"],
|
||||
checkable=False,
|
||||
)
|
||||
self._warning_immediate_volume_control.set_button_click_callback(lambda _: self._test_sound("WarningImmediateVolume"))
|
||||
|
||||
alert_volume_items = [
|
||||
self._disengage_volume_control,
|
||||
self._engage_volume_control,
|
||||
self._prompt_volume_control,
|
||||
self._prompt_distracted_volume_control,
|
||||
self._refuse_volume_control,
|
||||
self._warning_soft_volume_control,
|
||||
self._warning_immediate_volume_control,
|
||||
]
|
||||
|
||||
self._toggles["DisengageVolume"] = self._disengage_volume_control
|
||||
self._toggles["EngageVolume"] = self._engage_volume_control
|
||||
self._toggles["PromptVolume"] = self._prompt_volume_control
|
||||
self._toggles["PromptDistractedVolume"] = self._prompt_distracted_volume_control
|
||||
self._toggles["RefuseVolume"] = self._refuse_volume_control
|
||||
self._toggles["WarningSoftVolume"] = self._warning_soft_volume_control
|
||||
self._toggles["WarningImmediateVolume"] = self._warning_immediate_volume_control
|
||||
|
||||
self._alert_volume_scroller = Scroller(alert_volume_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_custom_alerts_panel(self):
|
||||
self._goat_scream_item = ListItem(
|
||||
title="Goat Scream",
|
||||
description="<b>Play the infamous \"Goat Scream\" when the steering controller reaches its limit.</b> Based on the \"Turn Exceeds Steering Limit\" event.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("GoatScream"),
|
||||
callback=lambda state: self._simple_toggle("GoatScream", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._green_light_alert_item = ListItem(
|
||||
title="Green Light Alert",
|
||||
description="<b>Play an alert when the model predicts a red light has turned green.</b><br><br><i><b>Disclaimer</b>: openpilot does not explicitly detect traffic lights. This alert is based on end-to-end model predictions from camera input and may trigger even when the light has not changed.</i>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("GreenLightAlert"),
|
||||
callback=lambda state: self._simple_toggle("GreenLightAlert", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._lead_departing_alert_item = ListItem(
|
||||
title="Lead Departing Alert",
|
||||
description="<b>Play an alert when the lead vehicle departs from a stop.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("LeadDepartingAlert"),
|
||||
callback=lambda state: self._simple_toggle("LeadDepartingAlert", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._loud_blindspot_alert_item = ListItem(
|
||||
title="Loud \"Car Detected in Blindspot\" Alert",
|
||||
description="<b>Play a louder alert if a vehicle is in the blind spot when attempting to change lanes.</b> Based on the \"Car Detected in Blindspot\" event.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("LoudBlindspotAlert"),
|
||||
callback=lambda state: self._simple_toggle("LoudBlindspotAlert", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._speed_limit_changed_alert_item = ListItem(
|
||||
title="Speed Limit Changed Alert",
|
||||
description="<b>Play an alert when the posted speed limit changes.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("SpeedLimitChangedAlert"),
|
||||
callback=lambda state: self._simple_toggle("SpeedLimitChangedAlert", state),
|
||||
),
|
||||
)
|
||||
|
||||
custom_alerts_items = [
|
||||
self._goat_scream_item,
|
||||
self._green_light_alert_item,
|
||||
self._lead_departing_alert_item,
|
||||
self._loud_blindspot_alert_item,
|
||||
self._speed_limit_changed_alert_item,
|
||||
]
|
||||
|
||||
self._toggles["GoatScream"] = self._goat_scream_item
|
||||
self._toggles["GreenLightAlert"] = self._green_light_alert_item
|
||||
self._toggles["LeadDepartingAlert"] = self._lead_departing_alert_item
|
||||
self._toggles["LoudBlindspotAlert"] = self._loud_blindspot_alert_item
|
||||
self._toggles["SpeedLimitChangedAlert"] = self._speed_limit_changed_alert_item
|
||||
|
||||
self._custom_alerts_scroller = Scroller(custom_alerts_items, line_separator=True, spacing=0)
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _initialize_sound_player(self):
|
||||
"""Initialize a Python subprocess for playing test sounds."""
|
||||
program = '''
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
import sys
|
||||
import wave
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
path, volume = line.strip().split('|')
|
||||
|
||||
sound_file = wave.open(path, 'rb')
|
||||
audio = np.frombuffer(sound_file.readframes(sound_file.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
|
||||
|
||||
sd.play(audio * float(volume), sound_file.getframerate())
|
||||
sd.wait()
|
||||
except Exception:
|
||||
pass
|
||||
'''
|
||||
|
||||
try:
|
||||
self._sound_player_process = subprocess.Popen(
|
||||
["python3", "-u", "-c", program],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception:
|
||||
self._sound_player_process = None
|
||||
|
||||
def _test_sound(self, key: str):
|
||||
"""Test a sound by playing it or triggering via params."""
|
||||
# Remove "Volume" suffix to get base alert name
|
||||
base_name = key.replace("Volume", "")
|
||||
|
||||
if self._started:
|
||||
# If driving, trigger via TestAlert param (handled by openpilot)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
# Convert to camelCase for TestAlert param
|
||||
camel_case_alert = base_name[0].lower() + base_name[1:]
|
||||
self._params_memory.put("TestAlert", camel_case_alert)
|
||||
else:
|
||||
# If parked, play directly via sound player process
|
||||
snake_case_alert = camel_to_snake(base_name)
|
||||
|
||||
# Check for custom theme sound first, then fall back to stock
|
||||
theme_path = THEME_SOUNDS_PATH / f"{snake_case_alert}.wav"
|
||||
stock_path = STOCK_SOUNDS_PATH / f"{snake_case_alert}.wav"
|
||||
|
||||
sound_path = theme_path if theme_path.exists() else stock_path
|
||||
|
||||
if not sound_path.exists():
|
||||
return
|
||||
|
||||
# Get volume from param (0-101, where 101 is auto)
|
||||
volume_param = self._params.get_float(key)
|
||||
if volume_param is None:
|
||||
volume_param = self._params.get_int(key) or 100
|
||||
|
||||
# Auto (101) defaults to 50%
|
||||
volume = volume_param / 100.0 if volume_param <= 100 else 0.5
|
||||
|
||||
self._play_sound(str(sound_path), volume)
|
||||
|
||||
def _play_sound(self, path: str, volume: float):
|
||||
"""Play a sound file at the specified volume."""
|
||||
if self._sound_player_process is None or self._sound_player_process.poll() is not None:
|
||||
self._initialize_sound_player()
|
||||
|
||||
if self._sound_player_process and self._sound_player_process.stdin:
|
||||
try:
|
||||
message = f"{path}|{volume}\n"
|
||||
self._sound_player_process.stdin.write(message.encode())
|
||||
self._sound_player_process.stdin.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _open_alert_volume_panel(self):
|
||||
self._current_panel = SubPanel.ALERT_VOLUME_CONTROL
|
||||
|
||||
def _open_custom_alerts_panel(self):
|
||||
self._current_panel = SubPanel.CUSTOM_ALERTS
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
|
||||
# Check visibility conditions for specific toggles
|
||||
# LoudBlindspotAlert only visible if car has BSM
|
||||
if hasattr(self._loud_blindspot_alert_item, "set_visible"):
|
||||
self._loud_blindspot_alert_item.set_visible(self._has_bsm)
|
||||
|
||||
# SpeedLimitChangedAlert visible if ShowSpeedLimits OR (hasOpenpilotLongitudinal AND SpeedLimitController)
|
||||
show_speed_limits = self._params.get_bool("ShowSpeedLimits")
|
||||
speed_limit_controller = self._params.get_bool("SpeedLimitController")
|
||||
slc_visible = show_speed_limits or (self._has_openpilot_longitudinal and speed_limit_controller)
|
||||
if hasattr(self._speed_limit_changed_alert_item, "set_visible"):
|
||||
self._speed_limit_changed_alert_item.set_visible(slc_visible)
|
||||
|
||||
def _load_car_capabilities(self):
|
||||
"""Load car capabilities from frogpilot variables."""
|
||||
try:
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles
|
||||
toggles = get_frogpilot_toggles()
|
||||
self._has_bsm = getattr(toggles, "has_bsm", False)
|
||||
self._has_openpilot_longitudinal = getattr(toggles, "has_openpilot_longitudinal", False)
|
||||
except Exception:
|
||||
self._has_bsm = False
|
||||
self._has_openpilot_longitudinal = False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._load_car_capabilities()
|
||||
self._update_toggles()
|
||||
self._started = ui_state.started
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
# Clean up sound player process
|
||||
if self._sound_player_process:
|
||||
try:
|
||||
self._sound_player_process.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
self._sound_player_process = None
|
||||
|
||||
def _render(self, rect):
|
||||
self._started = ui_state.started
|
||||
|
||||
if self._current_panel == SubPanel.ALERT_VOLUME_CONTROL:
|
||||
self._alert_volume_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.CUSTOM_ALERTS:
|
||||
self._custom_alerts_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,976 @@
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog, DialogResult
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
FrogPilotButtonToggleControl,
|
||||
FrogPilotConfirmationDialog,
|
||||
FrogPilotManageControl,
|
||||
)
|
||||
|
||||
THEME_PACKS_DIR = Path("/data/themes/theme_packs/")
|
||||
WHEELS_DIR = Path("/data/themes/steering_wheels/")
|
||||
|
||||
CUSTOM_THEME_KEYS = {
|
||||
"ColorScheme",
|
||||
"DistanceIconPack",
|
||||
"DownloadStatusLabel",
|
||||
"IconPack",
|
||||
"SignalAnimation",
|
||||
"SoundPack",
|
||||
"WheelIcon",
|
||||
}
|
||||
|
||||
HOLIDAY_THEMES = [
|
||||
"New Year's",
|
||||
"Valentine's Day",
|
||||
"St. Patrick's Day",
|
||||
"World Frog Day",
|
||||
"April Fools",
|
||||
"Easter",
|
||||
"May the Fourth",
|
||||
"Cinco de Mayo",
|
||||
"Stitch Day",
|
||||
"Fourth of July",
|
||||
"Halloween",
|
||||
"Thanksgiving",
|
||||
"Christmas",
|
||||
]
|
||||
|
||||
# Asset type configurations: (sub_folder, param_key, downloadable_param, download_key)
|
||||
ASSET_CONFIGS = {
|
||||
"ColorScheme": ("colors", "ColorScheme", "DownloadableColors", "ColorToDownload"),
|
||||
"DistanceIconPack": ("distance_icons", "DistanceIconPack", "DownloadableDistanceIcons", "DistanceIconToDownload"),
|
||||
"IconPack": ("icons", "IconPack", "DownloadableIcons", "IconToDownload"),
|
||||
"SignalAnimation": ("signals", "SignalAnimation", "DownloadableSignals", "SignalToDownload"),
|
||||
"SoundPack": ("sounds", "SoundPack", "DownloadableSounds", "SoundToDownload"),
|
||||
"WheelIcon": ("", "WheelIcon", "DownloadableWheels", "WheelToDownload"),
|
||||
}
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
CUSTOM_THEMES = 1
|
||||
|
||||
|
||||
def is_user_created_theme(theme_name: str) -> bool:
|
||||
"""Check if a theme is user-created."""
|
||||
return theme_name.endswith("-user_created")
|
||||
|
||||
|
||||
def normalize_theme_name(name: str) -> str:
|
||||
"""Normalize a theme name for file matching."""
|
||||
normalized = name.lower()
|
||||
normalized = re.sub(r'[()]', '-', normalized)
|
||||
normalized = re.sub(r'\s+', '-', normalized)
|
||||
normalized = re.sub(r'[^a-z0-9\-]', '', normalized)
|
||||
normalized = normalized.rstrip('-')
|
||||
return normalized
|
||||
|
||||
|
||||
def get_theme_display_name(param_key: str, params: Params) -> str:
|
||||
"""Get the display name for a theme from its stored param value."""
|
||||
value = params.get(param_key, encoding="utf-8") or ""
|
||||
if not value:
|
||||
return "Stock"
|
||||
|
||||
base_name = value
|
||||
|
||||
# Extract creator if present (after ~)
|
||||
creator = ""
|
||||
tilde_idx = base_name.find("~")
|
||||
if tilde_idx >= 0:
|
||||
creator = base_name[tilde_idx + 1:]
|
||||
base_name = base_name[:tilde_idx]
|
||||
|
||||
# Split on - or _ and capitalize each part
|
||||
separator = "-" if "-" in base_name else "_"
|
||||
parts = [p for p in base_name.split(separator) if p]
|
||||
parts = [p.capitalize() for p in parts]
|
||||
|
||||
# Format display name
|
||||
if "-" in base_name and len(parts) > 1:
|
||||
display_name = f"{parts[0]} ({' '.join(parts[1:])})"
|
||||
else:
|
||||
display_name = " ".join(parts)
|
||||
|
||||
# Add user created indicator
|
||||
if is_user_created_theme(value):
|
||||
display_name = display_name.split(" (")[0] + " 🌟"
|
||||
|
||||
# Add creator
|
||||
if creator:
|
||||
display_name += f" - by: {creator}"
|
||||
|
||||
return display_name
|
||||
|
||||
|
||||
def store_theme_name(input_name: str, param_key: str, params: Params) -> str:
|
||||
"""Store a theme name and return its display name."""
|
||||
output = input_name.lower()
|
||||
output = output.replace("(", "").replace(")", "").replace("'", "").replace(".", "")
|
||||
|
||||
# Use - for names with parentheses, _ otherwise
|
||||
if "(" in input_name:
|
||||
output = output.replace(" ", "-")
|
||||
else:
|
||||
output = output.replace(" ", "_")
|
||||
|
||||
# Handle user created marker
|
||||
output = output.replace("_🌟", "-user_created").replace(" 🌟", "-user_created")
|
||||
output = output.strip()
|
||||
|
||||
params.put(param_key, output)
|
||||
return get_theme_display_name(param_key, params)
|
||||
|
||||
|
||||
def get_theme_list(directory: Path, sub_folder: str, asset_param: str, params: Params, exclude_current: bool = True) -> list[str]:
|
||||
"""Get list of available themes from a directory."""
|
||||
use_files = not sub_folder
|
||||
current_asset = params.get(asset_param, encoding="utf-8") or "" if exclude_current else ""
|
||||
|
||||
theme_list = []
|
||||
|
||||
if not directory.exists():
|
||||
return theme_list
|
||||
|
||||
for entry in directory.iterdir():
|
||||
# Skip current asset
|
||||
if entry.stem == current_asset:
|
||||
continue
|
||||
|
||||
# For files mode, skip directories
|
||||
if use_files and entry.is_dir():
|
||||
continue
|
||||
|
||||
# For sub-folder mode, check if sub-folder exists
|
||||
if not use_files:
|
||||
target_path = entry / sub_folder
|
||||
if not target_path.exists():
|
||||
continue
|
||||
|
||||
base_name = entry.stem
|
||||
user_created = is_user_created_theme(base_name)
|
||||
if user_created:
|
||||
base_name = base_name.replace("-user_created", "")
|
||||
|
||||
# Extract creator
|
||||
creator = ""
|
||||
tilde_idx = base_name.find("~")
|
||||
if tilde_idx >= 0:
|
||||
creator = base_name[tilde_idx + 1:]
|
||||
base_name = base_name[:tilde_idx]
|
||||
|
||||
# Split and capitalize
|
||||
separator = "-" if "-" in base_name else "_"
|
||||
parts = [p for p in base_name.split(separator) if p]
|
||||
parts = [p.capitalize() for p in parts]
|
||||
|
||||
# Format display name
|
||||
if user_created:
|
||||
display_name = " ".join(parts)
|
||||
else:
|
||||
if len(parts) <= 1 or use_files:
|
||||
display_name = " ".join(parts)
|
||||
else:
|
||||
display_name = f"{parts[0]} ({' '.join(parts[1:])})"
|
||||
|
||||
if user_created:
|
||||
display_name += " 🌟"
|
||||
if creator:
|
||||
display_name += f" - by: {creator}"
|
||||
|
||||
theme_list.append(display_name)
|
||||
|
||||
return sorted(theme_list)
|
||||
|
||||
|
||||
def update_asset_param(asset_param: str, params: Params, value: str, add: bool):
|
||||
"""Update the downloadable asset list."""
|
||||
assets_str = params.get(asset_param, encoding="utf-8") or ""
|
||||
assets = [a for a in assets_str.split(",") if a]
|
||||
|
||||
if add:
|
||||
if value not in assets:
|
||||
assets.append(value)
|
||||
else:
|
||||
if value in assets:
|
||||
assets.remove(value)
|
||||
|
||||
assets.sort()
|
||||
params.put(asset_param, ",".join(assets))
|
||||
|
||||
|
||||
def download_theme_asset(input_name: str, download_key: str, downloadable_param: str, params: Params, params_memory: Params):
|
||||
"""Initiate a theme asset download."""
|
||||
output = input_name
|
||||
|
||||
# Handle creator suffix
|
||||
tilde_idx = output.find("~")
|
||||
if tilde_idx >= 0:
|
||||
output = output[:tilde_idx].lower() + "~" + output[tilde_idx + 1:]
|
||||
else:
|
||||
output = output.lower()
|
||||
|
||||
output = output.replace("(", "").replace(")", "")
|
||||
output = output.replace(" ", "-" if "(" in input_name else "_")
|
||||
|
||||
params_memory.put(download_key, output)
|
||||
|
||||
|
||||
def delete_theme_asset(directory: Path, sub_folder: str, downloadable_param: str, theme_to_delete: str, params: Params):
|
||||
"""Delete a theme asset."""
|
||||
use_files = not sub_folder
|
||||
|
||||
# Normalize the name for matching
|
||||
base_name = theme_to_delete.lower()
|
||||
base_name = re.sub(r'[()]', '-', base_name)
|
||||
base_name = base_name.replace(" ", "-")
|
||||
base_name = re.sub(r'[^a-z0-9\-]', '', base_name)
|
||||
base_name = base_name.rstrip('-')
|
||||
|
||||
base_underscore = base_name.replace("-", "_")
|
||||
|
||||
candidate_names = [
|
||||
base_name,
|
||||
base_name + "-user-created",
|
||||
base_underscore,
|
||||
base_underscore + "-user_created",
|
||||
]
|
||||
|
||||
if use_files:
|
||||
# Delete file
|
||||
for file in directory.iterdir():
|
||||
if not file.is_file():
|
||||
continue
|
||||
normalized_file = file.stem.lower().replace("_", "-")
|
||||
normalized_file = re.sub(r'[^a-z0-9\-~]', '', normalized_file)
|
||||
|
||||
if normalized_file in candidate_names:
|
||||
file.unlink()
|
||||
break
|
||||
else:
|
||||
# Delete directory
|
||||
for candidate in candidate_names:
|
||||
target_dir = directory / candidate / sub_folder
|
||||
if target_dir.exists():
|
||||
shutil.rmtree(target_dir.parent, ignore_errors=True)
|
||||
break
|
||||
|
||||
# Update downloadable list - add back to available downloads
|
||||
update_asset_param(downloadable_param, params, theme_to_delete, True)
|
||||
|
||||
|
||||
class FrogPilotThemePanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# State tracking
|
||||
self._cancelling_download = False
|
||||
self._finalizing_download = False
|
||||
self._online = False
|
||||
self._parked = True
|
||||
self._random_themes = False
|
||||
self._started = False
|
||||
self._theme_downloading = False
|
||||
|
||||
# Pending dialog action tracking
|
||||
self._pending_action = None # "delete", "download", "select", "delete_confirm", "custom_top", "custom_bottom", "clear_startup"
|
||||
self._pending_asset_type = None # "ColorScheme", "DistanceIconPack", etc.
|
||||
self._pending_selection = None # Selected item from first dialog
|
||||
|
||||
# Download state per asset type
|
||||
self._color_downloading = False
|
||||
self._distance_icon_downloading = False
|
||||
self._icon_downloading = False
|
||||
self._signal_downloading = False
|
||||
self._sound_downloading = False
|
||||
self._wheel_downloading = False
|
||||
|
||||
# Downloaded state (no more available to download)
|
||||
self._colors_downloaded = False
|
||||
self._distance_icons_downloaded = False
|
||||
self._icons_downloaded = False
|
||||
self._signals_downloaded = False
|
||||
self._sounds_downloaded = False
|
||||
self._wheels_downloaded = False
|
||||
|
||||
# Download status
|
||||
self._download_status = "Idle"
|
||||
|
||||
# Keyboard for text input
|
||||
self._keyboard = Keyboard()
|
||||
|
||||
self._build_main_panel()
|
||||
self._build_custom_themes_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_main_panel(self):
|
||||
self._custom_themes_control = FrogPilotManageControl(
|
||||
"CustomThemes",
|
||||
"Custom Themes",
|
||||
"<b>The overall look and feel of openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"../../frogpilot/assets/toggle_icons/icon_frog.png",
|
||||
)
|
||||
self._custom_themes_control.set_manage_callback(self._open_custom_themes_panel)
|
||||
|
||||
self._holiday_themes_item = ListItem(
|
||||
title="Holiday Themes",
|
||||
description="<b>Themes based on U.S. holidays.</b> Minor holidays last one day; major holidays (Christmas, Easter, Halloween) run for a full week.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HolidayThemes"),
|
||||
callback=lambda state: self._simple_toggle("HolidayThemes", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._rainbow_path_item = ListItem(
|
||||
title="Rainbow Path",
|
||||
description="<b>Color the driving path like a Mario Kart-style \"Rainbow Road\".</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("RainbowPath"),
|
||||
callback=lambda state: self._simple_toggle("RainbowPath", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._random_events_item = ListItem(
|
||||
title="Random Events",
|
||||
description="<b>Occasional on-screen effects triggered by driving conditions.</b> These are purely visual and don't impact how openpilot drives!",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("RandomEvents"),
|
||||
callback=lambda state: self._simple_toggle("RandomEvents", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._random_themes_control = FrogPilotButtonToggleControl(
|
||||
"RandomThemes",
|
||||
"Random Themes",
|
||||
"<b>Pick a random theme between each drive</b> from the themes you have downloaded. Great for variety without changing settings while driving.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_random_themes.png",
|
||||
button_params=["RandomThemesHolidays"],
|
||||
button_texts=["Include Holiday Themes"],
|
||||
)
|
||||
self._random_themes_control.set_toggle_callback(self._on_random_themes_toggle)
|
||||
|
||||
self._startup_alert_control = FrogPilotButtonsControl(
|
||||
"Startup Alert",
|
||||
"<b>Customize the \"Startup Alert\" message</b> shown at the start of each drive.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_message.png",
|
||||
button_texts=["STOCK", "FROGPILOT", "CUSTOM", "CLEAR"],
|
||||
)
|
||||
self._startup_alert_control.set_click_callback(self._on_startup_alert_click)
|
||||
self._update_startup_alert_buttons()
|
||||
|
||||
main_items = [
|
||||
self._custom_themes_control,
|
||||
self._holiday_themes_item,
|
||||
self._rainbow_path_item,
|
||||
self._random_events_item,
|
||||
self._random_themes_control,
|
||||
self._startup_alert_control,
|
||||
]
|
||||
|
||||
self._toggles["CustomThemes"] = self._custom_themes_control
|
||||
self._toggles["HolidayThemes"] = self._holiday_themes_item
|
||||
self._toggles["RainbowPath"] = self._rainbow_path_item
|
||||
self._toggles["RandomEvents"] = self._random_events_item
|
||||
self._toggles["RandomThemes"] = self._random_themes_control
|
||||
self._toggles["StartupAlert"] = self._startup_alert_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_custom_themes_panel(self):
|
||||
# Color Scheme
|
||||
self._color_scheme_control = FrogPilotButtonsControl(
|
||||
"Color Scheme",
|
||||
"<b>The color scheme used throughout openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"",
|
||||
button_texts=["DELETE", "DOWNLOAD", "SELECT"],
|
||||
)
|
||||
self._color_scheme_control.set_click_callback(self._on_color_scheme_click)
|
||||
self._color_scheme_control.set_value(get_theme_display_name("ColorScheme", self._params))
|
||||
|
||||
# Distance Icon Pack
|
||||
self._distance_icon_control = FrogPilotButtonsControl(
|
||||
"Distance Button",
|
||||
"<b>The distance button icons shown on the driving screen.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"",
|
||||
button_texts=["DELETE", "DOWNLOAD", "SELECT"],
|
||||
)
|
||||
self._distance_icon_control.set_click_callback(self._on_distance_icon_click)
|
||||
self._distance_icon_control.set_value(get_theme_display_name("DistanceIconPack", self._params))
|
||||
|
||||
# Icon Pack
|
||||
self._icon_pack_control = FrogPilotButtonsControl(
|
||||
"Icon Pack",
|
||||
"<b>The icon style used across openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"",
|
||||
button_texts=["DELETE", "DOWNLOAD", "SELECT"],
|
||||
)
|
||||
self._icon_pack_control.set_click_callback(self._on_icon_pack_click)
|
||||
self._icon_pack_control.set_value(get_theme_display_name("IconPack", self._params))
|
||||
|
||||
# Signal Animation
|
||||
self._signal_animation_control = FrogPilotButtonsControl(
|
||||
"Turn Signal",
|
||||
"<b>Themed turn-signal animations.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"",
|
||||
button_texts=["DELETE", "DOWNLOAD", "SELECT"],
|
||||
)
|
||||
self._signal_animation_control.set_click_callback(self._on_signal_animation_click)
|
||||
self._signal_animation_control.set_value(get_theme_display_name("SignalAnimation", self._params))
|
||||
|
||||
# Sound Pack
|
||||
self._sound_pack_control = FrogPilotButtonsControl(
|
||||
"Sound Pack",
|
||||
"<b>The sound pack used by openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"",
|
||||
button_texts=["DELETE", "DOWNLOAD", "SELECT"],
|
||||
)
|
||||
self._sound_pack_control.set_click_callback(self._on_sound_pack_click)
|
||||
self._sound_pack_control.set_value(get_theme_display_name("SoundPack", self._params))
|
||||
|
||||
# Wheel Icon
|
||||
self._wheel_icon_control = FrogPilotButtonsControl(
|
||||
"Steering Wheel",
|
||||
"<b>The steering-wheel icon</b> shown at the top-right of the driving screen. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!",
|
||||
"",
|
||||
button_texts=["DELETE", "DOWNLOAD", "SELECT"],
|
||||
)
|
||||
self._wheel_icon_control.set_click_callback(self._on_wheel_icon_click)
|
||||
self._wheel_icon_control.set_value(get_theme_display_name("WheelIcon", self._params))
|
||||
|
||||
# Download Status Label
|
||||
self._download_status_item = ListItem(
|
||||
title="Download Status",
|
||||
action_item=TextAction(lambda: self._download_status, color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
custom_theme_items = [
|
||||
self._color_scheme_control,
|
||||
self._distance_icon_control,
|
||||
self._icon_pack_control,
|
||||
self._signal_animation_control,
|
||||
self._sound_pack_control,
|
||||
self._wheel_icon_control,
|
||||
self._download_status_item,
|
||||
]
|
||||
|
||||
self._toggles["ColorScheme"] = self._color_scheme_control
|
||||
self._toggles["DistanceIconPack"] = self._distance_icon_control
|
||||
self._toggles["IconPack"] = self._icon_pack_control
|
||||
self._toggles["SignalAnimation"] = self._signal_animation_control
|
||||
self._toggles["SoundPack"] = self._sound_pack_control
|
||||
self._toggles["WheelIcon"] = self._wheel_icon_control
|
||||
self._toggles["DownloadStatusLabel"] = self._download_status_item
|
||||
|
||||
self._custom_themes_scroller = Scroller(custom_theme_items, line_separator=True, spacing=0)
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_random_themes_toggle(self, state: bool):
|
||||
self._params.put_bool("RandomThemes", state)
|
||||
update_frogpilot_toggles()
|
||||
self._random_themes = state
|
||||
|
||||
if state:
|
||||
gui_app.set_modal_overlay(alert_dialog(
|
||||
"\"Random Themes\" only works with downloaded themes, so make sure you download the themes you want it to use!"
|
||||
))
|
||||
|
||||
# Hide SELECT buttons and clear values
|
||||
self._color_scheme_control.set_value("")
|
||||
self._color_scheme_control.set_visible_button(2, False)
|
||||
self._distance_icon_control.set_value("")
|
||||
self._distance_icon_control.set_visible_button(2, False)
|
||||
self._icon_pack_control.set_value("")
|
||||
self._icon_pack_control.set_visible_button(2, False)
|
||||
self._signal_animation_control.set_value("")
|
||||
self._signal_animation_control.set_visible_button(2, False)
|
||||
self._sound_pack_control.set_value("")
|
||||
self._sound_pack_control.set_visible_button(2, False)
|
||||
self._wheel_icon_control.set_value("")
|
||||
self._wheel_icon_control.set_visible_button(2, False)
|
||||
else:
|
||||
# Show SELECT buttons and restore values
|
||||
self._color_scheme_control.set_value(get_theme_display_name("ColorScheme", self._params))
|
||||
self._color_scheme_control.set_visible_button(2, True)
|
||||
self._distance_icon_control.set_value(get_theme_display_name("DistanceIconPack", self._params))
|
||||
self._distance_icon_control.set_visible_button(2, True)
|
||||
self._icon_pack_control.set_value(get_theme_display_name("IconPack", self._params))
|
||||
self._icon_pack_control.set_visible_button(2, True)
|
||||
self._signal_animation_control.set_value(get_theme_display_name("SignalAnimation", self._params))
|
||||
self._signal_animation_control.set_visible_button(2, True)
|
||||
self._sound_pack_control.set_value(get_theme_display_name("SoundPack", self._params))
|
||||
self._sound_pack_control.set_visible_button(2, True)
|
||||
self._wheel_icon_control.set_value(get_theme_display_name("WheelIcon", self._params))
|
||||
self._wheel_icon_control.set_visible_button(2, True)
|
||||
|
||||
def _update_startup_alert_buttons(self):
|
||||
"""Update startup alert button states based on current values."""
|
||||
current_top = self._params.get("StartupMessageTop", encoding="utf-8") or ""
|
||||
current_bottom = self._params.get("StartupMessageBottom", encoding="utf-8") or ""
|
||||
|
||||
stock_top = "Be ready to take over at any time"
|
||||
stock_bottom = "Always keep hands on wheel and eyes on road"
|
||||
frogpilot_top = "Hop in and buckle up!"
|
||||
frogpilot_bottom = "Human-tested, frog-approved 🐸"
|
||||
|
||||
if current_top == stock_top and current_bottom == stock_bottom:
|
||||
self._startup_alert_control.set_checked_button(0)
|
||||
elif current_top == frogpilot_top and current_bottom == frogpilot_bottom:
|
||||
self._startup_alert_control.set_checked_button(1)
|
||||
elif current_top or current_bottom:
|
||||
self._startup_alert_control.set_checked_button(2)
|
||||
|
||||
def _on_startup_alert_click(self, button_id: int):
|
||||
stock_top = "Be ready to take over at any time"
|
||||
stock_bottom = "Always keep hands on wheel and eyes on road"
|
||||
frogpilot_top = "Hop in and buckle up!"
|
||||
frogpilot_bottom = "Human-tested, frog-approved 🐸"
|
||||
|
||||
if button_id == 0:
|
||||
# Stock
|
||||
self._params.put("StartupMessageTop", stock_top)
|
||||
self._params.put("StartupMessageBottom", stock_bottom)
|
||||
elif button_id == 1:
|
||||
# FrogPilot
|
||||
self._params.put("StartupMessageTop", frogpilot_top)
|
||||
self._params.put("StartupMessageBottom", frogpilot_bottom)
|
||||
elif button_id == 2:
|
||||
# Custom - show input dialog for top message
|
||||
self._pending_action = "custom_top"
|
||||
current_top = self._params.get("StartupMessageTop", encoding="utf-8") or ""
|
||||
self._keyboard.reset()
|
||||
self._keyboard.set_title("Enter the text for the top half")
|
||||
self._keyboard.set_text(current_top)
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
elif button_id == 3:
|
||||
# Clear - show confirmation
|
||||
self._pending_action = "clear_startup"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to completely reset your startup message?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
|
||||
def _get_control_for_asset(self, asset_type: str) -> FrogPilotButtonsControl:
|
||||
"""Get the control widget for an asset type."""
|
||||
controls = {
|
||||
"ColorScheme": self._color_scheme_control,
|
||||
"DistanceIconPack": self._distance_icon_control,
|
||||
"IconPack": self._icon_pack_control,
|
||||
"SignalAnimation": self._signal_animation_control,
|
||||
"SoundPack": self._sound_pack_control,
|
||||
"WheelIcon": self._wheel_icon_control,
|
||||
}
|
||||
return controls.get(asset_type)
|
||||
|
||||
def _get_downloading_attr(self, asset_type: str) -> str:
|
||||
"""Get the downloading attribute name for an asset type."""
|
||||
attrs = {
|
||||
"ColorScheme": "_color_downloading",
|
||||
"DistanceIconPack": "_distance_icon_downloading",
|
||||
"IconPack": "_icon_downloading",
|
||||
"SignalAnimation": "_signal_downloading",
|
||||
"SoundPack": "_sound_downloading",
|
||||
"WheelIcon": "_wheel_downloading",
|
||||
}
|
||||
return attrs.get(asset_type)
|
||||
|
||||
def _get_downloaded_attr(self, asset_type: str) -> str:
|
||||
"""Get the downloaded attribute name for an asset type."""
|
||||
attrs = {
|
||||
"ColorScheme": "_colors_downloaded",
|
||||
"DistanceIconPack": "_distance_icons_downloaded",
|
||||
"IconPack": "_icons_downloaded",
|
||||
"SignalAnimation": "_signals_downloaded",
|
||||
"SoundPack": "_sounds_downloaded",
|
||||
"WheelIcon": "_wheels_downloaded",
|
||||
}
|
||||
return attrs.get(asset_type)
|
||||
|
||||
def _handle_asset_click(self, button_id: int, asset_type: str):
|
||||
"""Generic handler for asset button clicks (DELETE, DOWNLOAD, SELECT)."""
|
||||
config = ASSET_CONFIGS[asset_type]
|
||||
sub_folder, param_key, downloadable_param, download_key = config
|
||||
|
||||
directory = WHEELS_DIR if asset_type == "WheelIcon" else THEME_PACKS_DIR
|
||||
downloading_attr = self._get_downloading_attr(asset_type)
|
||||
|
||||
if button_id == 0:
|
||||
# DELETE - show selection dialog
|
||||
theme_list = get_theme_list(directory, sub_folder, param_key, self._params)
|
||||
if not theme_list:
|
||||
gui_app.set_modal_overlay(alert_dialog(f"No {asset_type.lower()} available to delete."))
|
||||
return
|
||||
|
||||
self._pending_action = "delete"
|
||||
self._pending_asset_type = asset_type
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
f"Select a {asset_type.lower()} to delete",
|
||||
theme_list,
|
||||
))
|
||||
|
||||
elif button_id == 1:
|
||||
# DOWNLOAD or CANCEL
|
||||
if getattr(self, downloading_attr):
|
||||
# Cancel download
|
||||
self._cancelling_download = True
|
||||
self._params_memory.put_bool("CancelThemeDownload", True)
|
||||
|
||||
def reset_cancel():
|
||||
self._cancelling_download = False
|
||||
setattr(self, downloading_attr, False)
|
||||
self._theme_downloading = False
|
||||
self._params_memory.put_bool("CancelThemeDownload", False)
|
||||
|
||||
threading.Timer(2.5, reset_cancel).start()
|
||||
else:
|
||||
# Start download - show selection dialog
|
||||
downloadable_str = self._params.get(downloadable_param, encoding="utf-8") or ""
|
||||
downloadable = [d for d in downloadable_str.split(",") if d]
|
||||
|
||||
if not downloadable:
|
||||
gui_app.set_modal_overlay(alert_dialog(f"All {asset_type.lower()}s are already downloaded."))
|
||||
return
|
||||
|
||||
self._pending_action = "download"
|
||||
self._pending_asset_type = asset_type
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
f"Select a {asset_type.lower()} to download",
|
||||
downloadable,
|
||||
))
|
||||
|
||||
elif button_id == 2:
|
||||
# SELECT - show selection dialog
|
||||
theme_list = get_theme_list(directory, sub_folder, param_key, self._params, exclude_current=False)
|
||||
|
||||
# Add default options
|
||||
if asset_type == "SignalAnimation":
|
||||
theme_list.append("None")
|
||||
elif asset_type == "WheelIcon":
|
||||
theme_list.append("None")
|
||||
theme_list.append("Stock")
|
||||
else:
|
||||
theme_list.append("Stock")
|
||||
|
||||
theme_list.extend(HOLIDAY_THEMES)
|
||||
theme_list.sort()
|
||||
|
||||
current = get_theme_display_name(param_key, self._params)
|
||||
|
||||
self._pending_action = "select"
|
||||
self._pending_asset_type = asset_type
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
f"Select a {asset_type.lower()}",
|
||||
theme_list,
|
||||
current,
|
||||
))
|
||||
|
||||
def _on_color_scheme_click(self, button_id: int):
|
||||
self._handle_asset_click(button_id, "ColorScheme")
|
||||
|
||||
def _on_distance_icon_click(self, button_id: int):
|
||||
self._handle_asset_click(button_id, "DistanceIconPack")
|
||||
|
||||
def _on_icon_pack_click(self, button_id: int):
|
||||
self._handle_asset_click(button_id, "IconPack")
|
||||
|
||||
def _on_signal_animation_click(self, button_id: int):
|
||||
self._handle_asset_click(button_id, "SignalAnimation")
|
||||
|
||||
def _on_sound_pack_click(self, button_id: int):
|
||||
self._handle_asset_click(button_id, "SoundPack")
|
||||
|
||||
def _on_wheel_icon_click(self, button_id: int):
|
||||
self._handle_asset_click(button_id, "WheelIcon")
|
||||
|
||||
def _on_keyboard_result(self, result: DialogResult):
|
||||
"""Callback for keyboard modal overlay."""
|
||||
self.handle_dialog_result(result, self._keyboard.text)
|
||||
|
||||
def handle_dialog_result(self, result: DialogResult, selection: str = ""):
|
||||
"""Handle dialog results for all pending actions."""
|
||||
action = self._pending_action
|
||||
asset_type = self._pending_asset_type
|
||||
self._pending_action = None
|
||||
|
||||
if action == "delete":
|
||||
# First dialog - theme selection for delete
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
self._pending_asset_type = None
|
||||
return
|
||||
|
||||
# Show confirmation dialog
|
||||
self._pending_action = "delete_confirm"
|
||||
self._pending_selection = selection
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
f'Delete the "{selection}" {asset_type.lower()}?',
|
||||
"Delete",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
elif action == "delete_confirm":
|
||||
# Confirmation for delete
|
||||
if result != DialogResult.CONFIRM:
|
||||
self._pending_asset_type = None
|
||||
self._pending_selection = None
|
||||
return
|
||||
|
||||
selection = self._pending_selection
|
||||
self._pending_selection = None
|
||||
|
||||
config = ASSET_CONFIGS[asset_type]
|
||||
sub_folder, param_key, downloadable_param, download_key = config
|
||||
directory = WHEELS_DIR if asset_type == "WheelIcon" else THEME_PACKS_DIR
|
||||
|
||||
# Mark as not all downloaded anymore
|
||||
downloaded_attr = self._get_downloaded_attr(asset_type)
|
||||
setattr(self, downloaded_attr, False)
|
||||
|
||||
# Delete the asset
|
||||
delete_theme_asset(directory, sub_folder, downloadable_param, selection, self._params)
|
||||
self._pending_asset_type = None
|
||||
|
||||
elif action == "download":
|
||||
# Theme selection for download
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
self._pending_asset_type = None
|
||||
return
|
||||
|
||||
config = ASSET_CONFIGS[asset_type]
|
||||
sub_folder, param_key, downloadable_param, download_key = config
|
||||
|
||||
# Set downloading flags
|
||||
downloading_attr = self._get_downloading_attr(asset_type)
|
||||
setattr(self, downloading_attr, True)
|
||||
self._theme_downloading = True
|
||||
|
||||
self._params_memory.put("ThemeDownloadProgress", "Downloading...")
|
||||
self._download_status = "Downloading..."
|
||||
|
||||
# Initiate download
|
||||
download_theme_asset(selection, download_key, downloadable_param, self._params, self._params_memory)
|
||||
self._pending_asset_type = None
|
||||
|
||||
elif action == "select":
|
||||
# Theme selection
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
self._pending_asset_type = None
|
||||
return
|
||||
|
||||
config = ASSET_CONFIGS[asset_type]
|
||||
sub_folder, param_key, downloadable_param, download_key = config
|
||||
|
||||
# Store the theme and update display
|
||||
control = self._get_control_for_asset(asset_type)
|
||||
display_name = store_theme_name(selection, param_key, self._params)
|
||||
control.set_value(display_name)
|
||||
self._pending_asset_type = None
|
||||
|
||||
elif action == "custom_top":
|
||||
# Custom startup message - top line
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
return
|
||||
|
||||
self._params.put("StartupMessageTop", selection.strip())
|
||||
|
||||
# Now show dialog for bottom line
|
||||
self._pending_action = "custom_bottom"
|
||||
current_bottom = self._params.get("StartupMessageBottom", encoding="utf-8") or ""
|
||||
self._keyboard.reset()
|
||||
self._keyboard.set_title("Enter the text for the bottom half")
|
||||
self._keyboard.set_text(current_bottom)
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
|
||||
elif action == "custom_bottom":
|
||||
# Custom startup message - bottom line
|
||||
if result == DialogResult.CONFIRM and selection:
|
||||
self._params.put("StartupMessageBottom", selection.strip())
|
||||
self._update_startup_alert_buttons()
|
||||
|
||||
elif action == "clear_startup":
|
||||
# Clear startup message confirmation
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.remove("StartupMessageTop")
|
||||
self._params.remove("StartupMessageBottom")
|
||||
self._startup_alert_control.clear_checked_buttons()
|
||||
|
||||
def _translate_progress(self, progress: str) -> str:
|
||||
"""Translate download progress messages."""
|
||||
translations = {
|
||||
"Download cancelled...": "Download cancelled...",
|
||||
"Download failed...": "Download failed...",
|
||||
"Downloaded!": "Downloaded!",
|
||||
"Downloading...": "Downloading...",
|
||||
"GitHub and GitLab are offline...": "GitHub and GitLab are offline...",
|
||||
"Repository unavailable": "Repository unavailable",
|
||||
"Unpacking theme...": "Unpacking theme...",
|
||||
"Verifying authenticity...": "Verifying authenticity...",
|
||||
}
|
||||
|
||||
if progress in translations:
|
||||
return translations[progress]
|
||||
if progress.endswith("%"):
|
||||
return progress
|
||||
|
||||
return "Idle"
|
||||
|
||||
def _update_download_state(self):
|
||||
"""Update UI based on download progress."""
|
||||
if self._finalizing_download:
|
||||
return
|
||||
|
||||
if not self._theme_downloading:
|
||||
return
|
||||
|
||||
progress = self._params_memory.get("ThemeDownloadProgress", encoding="utf-8") or ""
|
||||
download_failed = bool(re.search(r"cancelled|exists|failed|offline", progress, re.IGNORECASE))
|
||||
|
||||
if progress and progress != "Downloading...":
|
||||
self._download_status = self._translate_progress(progress)
|
||||
|
||||
if progress == "Downloaded!" or download_failed:
|
||||
self._finalizing_download = True
|
||||
|
||||
def finalize():
|
||||
self._color_downloading = False
|
||||
self._distance_icon_downloading = False
|
||||
self._finalizing_download = False
|
||||
self._icon_downloading = False
|
||||
self._signal_downloading = False
|
||||
self._sound_downloading = False
|
||||
self._theme_downloading = False
|
||||
self._wheel_downloading = False
|
||||
|
||||
# Update downloaded states
|
||||
self._colors_downloaded = not self._params.get("DownloadableColors", encoding="utf-8")
|
||||
self._distance_icons_downloaded = not self._params.get("DownloadableDistanceIcons", encoding="utf-8")
|
||||
self._icons_downloaded = not self._params.get("DownloadableIcons", encoding="utf-8")
|
||||
self._signals_downloaded = not self._params.get("DownloadableSignals", encoding="utf-8")
|
||||
self._sounds_downloaded = not self._params.get("DownloadableSounds", encoding="utf-8")
|
||||
self._wheels_downloaded = not self._params.get("DownloadableWheels", encoding="utf-8")
|
||||
|
||||
self._params_memory.remove("CancelThemeDownload")
|
||||
self._params_memory.remove("ThemeDownloadProgress")
|
||||
|
||||
self._download_status = "Idle"
|
||||
|
||||
threading.Timer(2.5, finalize).start()
|
||||
|
||||
def _update_button_states(self):
|
||||
"""Update button enabled/visible states."""
|
||||
# Helper for updating each asset control
|
||||
def update_asset_buttons(control, downloading, downloaded):
|
||||
control.set_text(1, "CANCEL" if downloading else "DOWNLOAD")
|
||||
control.set_enabled_buttons(0, not self._theme_downloading)
|
||||
can_download = (self._online and
|
||||
(not self._theme_downloading or downloading) and
|
||||
not self._cancelling_download and
|
||||
not self._finalizing_download and
|
||||
not downloaded and
|
||||
self._parked)
|
||||
control.set_enabled_buttons(1, can_download)
|
||||
control.set_enabled_buttons(2, not self._theme_downloading)
|
||||
|
||||
update_asset_buttons(self._color_scheme_control, self._color_downloading, self._colors_downloaded)
|
||||
update_asset_buttons(self._distance_icon_control, self._distance_icon_downloading, self._distance_icons_downloaded)
|
||||
update_asset_buttons(self._icon_pack_control, self._icon_downloading, self._icons_downloaded)
|
||||
update_asset_buttons(self._signal_animation_control, self._signal_downloading, self._signals_downloaded)
|
||||
update_asset_buttons(self._sound_pack_control, self._sound_downloading, self._sounds_downloaded)
|
||||
update_asset_buttons(self._wheel_icon_control, self._wheel_downloading, self._wheels_downloaded)
|
||||
|
||||
def _open_custom_themes_panel(self):
|
||||
self._current_panel = SubPanel.CUSTOM_THEMES
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
|
||||
# DistanceIconPack only visible if QOLVisuals AND OnroadDistanceButton
|
||||
qol_visuals = self._params.get_bool("QOLVisuals")
|
||||
onroad_distance_button = self._params.get_bool("OnroadDistanceButton")
|
||||
if hasattr(self._distance_icon_control, "set_visible"):
|
||||
self._distance_icon_control.set_visible(qol_visuals and onroad_distance_button)
|
||||
|
||||
# RandomThemes only visible if CustomThemes enabled
|
||||
custom_themes = self._params.get_bool("CustomThemes")
|
||||
if hasattr(self._random_themes_control, "set_visible"):
|
||||
self._random_themes_control.set_visible(custom_themes)
|
||||
|
||||
def _load_downloaded_states(self):
|
||||
"""Load initial downloaded states."""
|
||||
self._colors_downloaded = not self._params.get("DownloadableColors", encoding="utf-8")
|
||||
self._distance_icons_downloaded = not self._params.get("DownloadableDistanceIcons", encoding="utf-8")
|
||||
self._icons_downloaded = not self._params.get("DownloadableIcons", encoding="utf-8")
|
||||
self._signals_downloaded = not self._params.get("DownloadableSignals", encoding="utf-8")
|
||||
self._sounds_downloaded = not self._params.get("DownloadableSounds", encoding="utf-8")
|
||||
self._wheels_downloaded = not self._params.get("DownloadableWheels", encoding="utf-8")
|
||||
|
||||
self._random_themes = self._params.get_bool("RandomThemes")
|
||||
|
||||
if self._random_themes:
|
||||
# Hide SELECT buttons and clear values
|
||||
self._color_scheme_control.set_value("")
|
||||
self._color_scheme_control.set_visible_button(2, False)
|
||||
self._distance_icon_control.set_value("")
|
||||
self._distance_icon_control.set_visible_button(2, False)
|
||||
self._icon_pack_control.set_value("")
|
||||
self._icon_pack_control.set_visible_button(2, False)
|
||||
self._signal_animation_control.set_value("")
|
||||
self._signal_animation_control.set_visible_button(2, False)
|
||||
self._sound_pack_control.set_value("")
|
||||
self._sound_pack_control.set_visible_button(2, False)
|
||||
self._wheel_icon_control.set_value("")
|
||||
self._wheel_icon_control.set_visible_button(2, False)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._load_downloaded_states()
|
||||
self._update_toggles()
|
||||
self._update_startup_alert_buttons()
|
||||
self._started = ui_state.started
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
self._started = ui_state.started
|
||||
self._parked = not self._started
|
||||
|
||||
# Update download state
|
||||
self._update_download_state()
|
||||
self._update_button_states()
|
||||
|
||||
if self._current_panel == SubPanel.CUSTOM_THEMES:
|
||||
self._custom_themes_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,376 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog, DialogResult
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ButtonAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
)
|
||||
|
||||
ERROR_LOG_PATH = Path("/data/error_logs/error.txt")
|
||||
|
||||
# Keys that should NOT be reset
|
||||
EXCLUDED_KEYS = {
|
||||
"AvailableModels",
|
||||
"AvailableModelNames",
|
||||
"FrogPilotStats",
|
||||
"GithubSshKeys",
|
||||
"GithubUsername",
|
||||
"MapBoxRequests",
|
||||
"ModelDrivesAndScores",
|
||||
"OverpassRequests",
|
||||
"SpeedLimits",
|
||||
"SpeedLimitsFiltered",
|
||||
"UpdaterAvailableBranches",
|
||||
}
|
||||
|
||||
REPORT_MESSAGES = [
|
||||
"Acceleration feels harsh or jerky",
|
||||
"An alert was unclear and I'm not sure what it meant",
|
||||
"Braking is too sudden or uncomfortable",
|
||||
"I'm not sure if this is normal or a bug:",
|
||||
"My steering wheel buttons aren't working",
|
||||
"openpilot disengages when I don't expect it",
|
||||
"openpilot feels sluggish or slow to respond",
|
||||
"Something else (please describe)",
|
||||
]
|
||||
|
||||
|
||||
class FrogPilotUtilitiesPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._params_memory = Params("", True)
|
||||
self._toggles = {}
|
||||
|
||||
# State tracking
|
||||
self._flash_status = ""
|
||||
self._reset_status = ""
|
||||
self._online = False
|
||||
|
||||
# Pending dialog action tracking
|
||||
self._pending_action = None # "flash_panda", "report_select", "report_extra", "report_discord", "reset_default", "reset_stock"
|
||||
self._pending_data = {}
|
||||
|
||||
# Keyboard for text input
|
||||
self._keyboard = Keyboard()
|
||||
|
||||
self._build_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_panel(self):
|
||||
# Debug Mode Toggle
|
||||
self._debug_mode_item = ListItem(
|
||||
title="Debug Mode",
|
||||
description="<b>Use all of FrogPilot's developer metrics on your next drive</b> to diagnose issues and improve bug reports.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("DebugMode"),
|
||||
callback=lambda state: self._simple_toggle("DebugMode", state),
|
||||
),
|
||||
)
|
||||
|
||||
# Flash Panda Button
|
||||
self._flash_panda_control = FrogPilotButtonsControl(
|
||||
"Flash Panda",
|
||||
"<b>Flash the latest, official firmware onto your Panda device</b> to restore core functionality, fix bugs, or ensure you have the most up-to-date software.",
|
||||
"",
|
||||
button_texts=["FLASH"],
|
||||
)
|
||||
self._flash_panda_control.set_click_callback(self._on_flash_panda_click)
|
||||
|
||||
# Force Drive State Buttons
|
||||
self._force_drive_state_control = FrogPilotButtonsControl(
|
||||
"Force Drive State",
|
||||
"<b>Force openpilot to be offroad or onroad.</b>",
|
||||
"",
|
||||
button_texts=["OFFROAD", "ONROAD", "OFF"],
|
||||
)
|
||||
self._force_drive_state_control.set_click_callback(self._on_force_drive_state_click)
|
||||
self._force_drive_state_control.set_checked_button(2)
|
||||
|
||||
# Report Issue Button
|
||||
self._report_issue_control = FrogPilotButtonsControl(
|
||||
"Report a Bug or an Issue",
|
||||
"<b>Send a bug report</b> so we can help fix the problem!",
|
||||
"",
|
||||
button_texts=["REPORT"],
|
||||
)
|
||||
self._report_issue_control.set_click_callback(self._on_report_issue_click)
|
||||
|
||||
# Reset Toggles to Default Button
|
||||
self._reset_default_control = FrogPilotButtonsControl(
|
||||
"Reset Toggles to Default",
|
||||
"<b>Reset all toggles to their default values.</b>",
|
||||
"",
|
||||
button_texts=["RESET"],
|
||||
)
|
||||
self._reset_default_control.set_click_callback(self._on_reset_default_click)
|
||||
|
||||
# Reset Toggles to Stock Button
|
||||
self._reset_stock_control = FrogPilotButtonsControl(
|
||||
"Reset Toggles to Stock openpilot",
|
||||
"<b>Reset all toggles to match stock openpilot.</b>",
|
||||
"",
|
||||
button_texts=["RESET"],
|
||||
)
|
||||
self._reset_stock_control.set_click_callback(self._on_reset_stock_click)
|
||||
|
||||
items = [
|
||||
self._debug_mode_item,
|
||||
self._flash_panda_control,
|
||||
self._force_drive_state_control,
|
||||
self._report_issue_control,
|
||||
self._reset_default_control,
|
||||
self._reset_stock_control,
|
||||
]
|
||||
|
||||
self._toggles["DebugMode"] = self._debug_mode_item
|
||||
self._toggles["FlashPanda"] = self._flash_panda_control
|
||||
self._toggles["ForceDriveState"] = self._force_drive_state_control
|
||||
self._toggles["ReportIssue"] = self._report_issue_control
|
||||
self._toggles["ResetDefault"] = self._reset_default_control
|
||||
self._toggles["ResetStock"] = self._reset_stock_control
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_flash_panda_click(self, button_id: int):
|
||||
self._pending_action = "flash_panda"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to flash the Panda firmware?",
|
||||
"Flash",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def _do_flash_panda(self):
|
||||
"""Flash panda firmware in a background thread."""
|
||||
def flash_thread():
|
||||
self._flash_panda_control.set_enabled(False)
|
||||
self._flash_panda_control.set_value("Flashing...")
|
||||
|
||||
self._params_memory.put_bool("FlashPanda", True)
|
||||
|
||||
# Wait for flash to complete
|
||||
while self._params_memory.get_bool("FlashPanda"):
|
||||
time.sleep(0.05) # UI_FREQ equivalent
|
||||
|
||||
self._flash_panda_control.set_value("Flashed!")
|
||||
time.sleep(2.5)
|
||||
|
||||
self._flash_panda_control.set_value("Rebooting...")
|
||||
time.sleep(2.5)
|
||||
|
||||
HARDWARE.reboot()
|
||||
|
||||
threading.Thread(target=flash_thread, daemon=True).start()
|
||||
|
||||
def _on_force_drive_state_click(self, button_id: int):
|
||||
if button_id == 0:
|
||||
# OFFROAD
|
||||
self._params.put_bool("ForceOffroad", True)
|
||||
self._params.put_bool("ForceOnroad", False)
|
||||
elif button_id == 1:
|
||||
# ONROAD - copy persistent car params
|
||||
car_params = self._params.get("CarParamsPersistent")
|
||||
if car_params:
|
||||
self._params.put("CarParams", car_params)
|
||||
|
||||
frogpilot_car_params = self._params.get("FrogPilotCarParamsPersistent")
|
||||
if frogpilot_car_params:
|
||||
self._params.put("FrogPilotCarParams", frogpilot_car_params)
|
||||
|
||||
self._params.put_bool("ForceOffroad", False)
|
||||
self._params.put_bool("ForceOnroad", True)
|
||||
elif button_id == 2:
|
||||
# OFF
|
||||
self._params.put_bool("ForceOffroad", False)
|
||||
self._params.put_bool("ForceOnroad", False)
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_report_issue_click(self, button_id: int):
|
||||
# Check if online (would need to be wired up properly to frogpilot_scene.online)
|
||||
# For now, we'll proceed with the report flow
|
||||
|
||||
# Build report messages list
|
||||
messages = list(REPORT_MESSAGES)
|
||||
|
||||
# Add crash option if error log exists
|
||||
if ERROR_LOG_PATH.exists():
|
||||
messages.insert(0, "I saw an alert that said \"openpilot crashed\"")
|
||||
|
||||
self._pending_action = "report_select"
|
||||
self._pending_data = {}
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"What's going on?",
|
||||
messages,
|
||||
))
|
||||
|
||||
def _on_reset_default_click(self, button_id: int):
|
||||
self._pending_action = "reset_default"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to reset all toggles to their default values?",
|
||||
"Reset",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def _on_reset_stock_click(self, button_id: int):
|
||||
self._pending_action = "reset_stock"
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to reset all toggles to match stock openpilot?",
|
||||
"Reset",
|
||||
"Cancel",
|
||||
))
|
||||
|
||||
def _do_reset_toggles(self, use_stock: bool):
|
||||
"""Reset toggles in a background thread."""
|
||||
control = self._reset_stock_control if use_stock else self._reset_default_control
|
||||
|
||||
def reset_thread():
|
||||
control.set_enabled(False)
|
||||
control.set_value("Resetting...")
|
||||
|
||||
all_keys = self._params.all_keys()
|
||||
|
||||
for key in all_keys:
|
||||
if key in EXCLUDED_KEYS:
|
||||
continue
|
||||
|
||||
try:
|
||||
if use_stock:
|
||||
stock_value = self._params.get_stock_value(key)
|
||||
if stock_value is not None:
|
||||
self._params.put(key, stock_value)
|
||||
else:
|
||||
default_value = self._params.get_key_default_value(key)
|
||||
if default_value is not None:
|
||||
self._params.put(key, default_value)
|
||||
except Exception:
|
||||
# Skip keys that don't have default/stock values
|
||||
pass
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
control.set_value("Reset!")
|
||||
time.sleep(2.5)
|
||||
|
||||
control.set_value("")
|
||||
control.set_enabled(True)
|
||||
|
||||
threading.Thread(target=reset_thread, daemon=True).start()
|
||||
|
||||
def _on_keyboard_result(self, result: DialogResult):
|
||||
"""Callback for keyboard modal overlay."""
|
||||
self.handle_dialog_result(result, self._keyboard.text)
|
||||
|
||||
def handle_dialog_result(self, result: DialogResult, selection: str = ""):
|
||||
"""Handle dialog results for all pending actions."""
|
||||
action = self._pending_action
|
||||
self._pending_action = None
|
||||
|
||||
if action == "flash_panda":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._do_flash_panda()
|
||||
|
||||
elif action == "report_select":
|
||||
# Report issue - first dialog (issue selection)
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
self._pending_data = {}
|
||||
return
|
||||
|
||||
self._pending_data["selected_issue"] = selection
|
||||
|
||||
# Check if we need extra input
|
||||
if "crashed" in selection.lower() or "not sure" in selection.lower() or "something else" in selection.lower():
|
||||
self._pending_action = "report_extra"
|
||||
self._keyboard.reset()
|
||||
self._keyboard.set_title("Please describe what's happening")
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
else:
|
||||
# Skip to discord username
|
||||
self._pending_action = "report_discord"
|
||||
current_discord = self._params.get("DiscordUsername", encoding="utf-8") or ""
|
||||
self._keyboard.reset()
|
||||
self._keyboard.set_title("What's your Discord username?")
|
||||
self._keyboard.set_text(current_discord)
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
|
||||
elif action == "report_extra":
|
||||
# Extra description for the issue
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
self._pending_data = {}
|
||||
return
|
||||
|
||||
# Append extra description to selected issue
|
||||
self._pending_data["selected_issue"] += " \u2014 " + selection.strip()
|
||||
|
||||
# Now get discord username
|
||||
self._pending_action = "report_discord"
|
||||
current_discord = self._params.get("DiscordUsername", encoding="utf-8") or ""
|
||||
self._keyboard.reset()
|
||||
self._keyboard.set_title("What's your Discord username?")
|
||||
self._keyboard.set_text(current_discord)
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_keyboard_result)
|
||||
|
||||
elif action == "report_discord":
|
||||
# Discord username input
|
||||
discord_user = selection.strip() if result == DialogResult.CONFIRM else ""
|
||||
|
||||
# Create report data
|
||||
report_data = {
|
||||
"DiscordUser": discord_user,
|
||||
"Issue": self._pending_data.get("selected_issue", ""),
|
||||
}
|
||||
|
||||
# Save discord username and report
|
||||
if discord_user:
|
||||
self._params.put_nonblocking("DiscordUsername", discord_user)
|
||||
self._params_memory.put("IssueReported", json.dumps(report_data))
|
||||
|
||||
self._pending_data = {}
|
||||
|
||||
# Show confirmation
|
||||
gui_app.set_modal_overlay(alert_dialog(
|
||||
"Report Sent! Thanks for letting us know!"
|
||||
))
|
||||
|
||||
elif action == "reset_default":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._do_reset_toggles(use_stock=False)
|
||||
|
||||
elif action == "reset_stock":
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._do_reset_toggles(use_stock=True)
|
||||
|
||||
def _update_toggles(self):
|
||||
# Report Issue button only visible for FrogAI repo
|
||||
git_remote = self._params.get("GitRemote", encoding="utf-8") or ""
|
||||
is_frogai = git_remote.lower() == "https://github.com/frogai/openpilot.git"
|
||||
if hasattr(self._report_issue_control, "set_visible"):
|
||||
self._report_issue_control.set_visible(is_frogai)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
@@ -0,0 +1,723 @@
|
||||
import re
|
||||
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ButtonAction, TextAction, ITEM_TEXT_VALUE_COLOR
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
FrogPilotButtonToggleControl,
|
||||
FrogPilotConfirmationDialog,
|
||||
FrogPilotManageControl,
|
||||
FrogPilotParamValueControl,
|
||||
FrogPilotParamValueButtonControl,
|
||||
)
|
||||
|
||||
OPENDBC_PATH = Path("/data/openpilot/opendbc/car")
|
||||
|
||||
# Map car makes to their parent brand folder in opendbc
|
||||
MAKE_TO_FOLDER = {
|
||||
"acura": "honda",
|
||||
"audi": "volkswagen",
|
||||
"buick": "gm",
|
||||
"cadillac": "gm",
|
||||
"chevrolet": "gm",
|
||||
"chrysler": "chrysler",
|
||||
"cupra": "volkswagen",
|
||||
"dodge": "chrysler",
|
||||
"ford": "ford",
|
||||
"genesis": "hyundai",
|
||||
"gmc": "gm",
|
||||
"holden": "gm",
|
||||
"honda": "honda",
|
||||
"hyundai": "hyundai",
|
||||
"jeep": "chrysler",
|
||||
"kia": "hyundai",
|
||||
"lexus": "toyota",
|
||||
"lincoln": "ford",
|
||||
"man": "volkswagen",
|
||||
"mazda": "mazda",
|
||||
"nissan": "nissan",
|
||||
"peugeot": "psa",
|
||||
"ram": "chrysler",
|
||||
"rivian": "rivian",
|
||||
"seat": "volkswagen",
|
||||
"škoda": "volkswagen",
|
||||
"subaru": "subaru",
|
||||
"tesla": "tesla",
|
||||
"toyota": "toyota",
|
||||
"volkswagen": "volkswagen",
|
||||
}
|
||||
|
||||
CAR_MAKES = [
|
||||
"Acura", "Audi", "Buick", "Cadillac", "Chevrolet", "Chrysler", "CUPRA",
|
||||
"Dodge", "Ford", "Genesis", "GMC", "Holden", "Honda", "Hyundai", "Jeep",
|
||||
"Kia", "Lexus", "Lincoln", "MAN", "Mazda", "Nissan", "Peugeot", "Ram",
|
||||
"Rivian", "SEAT", "Škoda", "Subaru", "Tesla", "Toyota", "Volkswagen",
|
||||
]
|
||||
|
||||
GM_KEYS = {"VoltSNG"}
|
||||
HKG_KEYS = {"TacoTuneHacks"}
|
||||
SUBARU_KEYS = {"SubaruSNG"}
|
||||
TOYOTA_KEYS = {"ClusterOffset", "FrogsGoMoosTweak", "LockDoorsTimer", "SNGHack", "ToyotaDoors"}
|
||||
LONGITUDINAL_KEYS = {"FrogsGoMoosTweak", "SNGHack", "VoltSNG"}
|
||||
VEHICLE_INFO_KEYS = {"BlindSpotSupport", "HardwareDetected", "OpenpilotLongitudinal", "PedalSupport", "RadarSupport", "SDSUSupport", "SNGSupport"}
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
GM = 1
|
||||
HKG = 2
|
||||
SUBARU = 3
|
||||
TOYOTA = 4
|
||||
VEHICLE_INFO = 5
|
||||
|
||||
|
||||
def get_car_names(car_make: str) -> tuple[list[str], dict[str, str]]:
|
||||
"""
|
||||
Parse opendbc values.py to get car names for a given make.
|
||||
Returns (car_names_list, car_name_to_platform_map).
|
||||
"""
|
||||
car_names = []
|
||||
car_models = {}
|
||||
|
||||
folder = MAKE_TO_FOLDER.get(car_make.lower(), "")
|
||||
if not folder:
|
||||
return car_names, car_models
|
||||
|
||||
values_path = OPENDBC_PATH / folder / "values.py"
|
||||
if not values_path.exists():
|
||||
return car_names, car_models
|
||||
|
||||
try:
|
||||
content = values_path.read_text()
|
||||
except Exception:
|
||||
return car_names, car_models
|
||||
|
||||
# Remove comments and footnotes
|
||||
content = re.sub(r'#[^\n]*', '', content)
|
||||
content = re.sub(r'footnotes=\[[^\]]*\],\s*', '', content)
|
||||
|
||||
# Find platform definitions: PLATFORM_NAME = SomeClass(
|
||||
platform_pattern = re.compile(r'(\w+)\s*=\s*\w+\s*\(')
|
||||
platforms = []
|
||||
for match in platform_pattern.finditer(content):
|
||||
platforms.append((match.start(), match.group(1)))
|
||||
platforms.append((len(content), ""))
|
||||
|
||||
# Find car names: CarDocs*("Car Name"
|
||||
car_name_pattern = re.compile(r'CarDocs\w*\s*\(\s*"([^"]+)"')
|
||||
lower_make = car_make.lower()
|
||||
|
||||
for i in range(len(platforms) - 1):
|
||||
start = platforms[i][0]
|
||||
end = platforms[i + 1][0]
|
||||
platform_name = platforms[i][1]
|
||||
|
||||
section = content[start:end]
|
||||
|
||||
for match in car_name_pattern.finditer(section):
|
||||
car_name = match.group(1)
|
||||
if car_name.lower().startswith(lower_make):
|
||||
car_models[car_name] = platform_name
|
||||
car_names.append(car_name)
|
||||
|
||||
car_names.sort(key=str.lower)
|
||||
return car_names, car_models
|
||||
|
||||
|
||||
def build_lock_timer_labels() -> dict[int, str]:
|
||||
"""Build labels for lock doors timer (0-300 seconds)."""
|
||||
labels = {}
|
||||
for i in range(0, 301):
|
||||
if i == 0:
|
||||
labels[i] = "Never"
|
||||
elif i == 1:
|
||||
labels[i] = "1 second"
|
||||
else:
|
||||
labels[i] = f"{i} seconds"
|
||||
return labels
|
||||
|
||||
|
||||
class FrogPilotVehiclesPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._params = Params()
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# Car model mapping (car_name -> platform)
|
||||
self._car_models: dict[str, str] = {}
|
||||
|
||||
# State tracking
|
||||
self._started = False
|
||||
|
||||
# Car capabilities (loaded from frogpilot_variables)
|
||||
self._has_bsm = False
|
||||
self._has_openpilot_longitudinal = False
|
||||
self._has_pedal = False
|
||||
self._has_radar = False
|
||||
self._has_sdsu = False
|
||||
self._has_sng = False
|
||||
self._has_zss = False
|
||||
self._can_use_pedal = False
|
||||
self._can_use_sdsu = False
|
||||
self._has_alpha_longitudinal = False
|
||||
self._openpilot_longitudinal_disabled = False
|
||||
|
||||
# Car brand flags
|
||||
self._is_gm = False
|
||||
self._is_hkg = False
|
||||
self._is_hkg_canfd = False
|
||||
self._is_subaru = False
|
||||
self._is_toyota = False
|
||||
self._is_volt = False
|
||||
|
||||
self._build_main_panel()
|
||||
self._build_gm_panel()
|
||||
self._build_hkg_panel()
|
||||
self._build_subaru_panel()
|
||||
self._build_toyota_panel()
|
||||
self._build_vehicle_info_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_main_panel(self):
|
||||
# Car Make Selection
|
||||
self._car_make_item = ListItem(
|
||||
title="Car Make",
|
||||
action_item=ButtonAction(
|
||||
text="SELECT",
|
||||
callback=self._on_car_make_click,
|
||||
),
|
||||
)
|
||||
|
||||
# Car Model Selection
|
||||
self._car_model_item = ListItem(
|
||||
title="Car Model",
|
||||
action_item=ButtonAction(
|
||||
text="SELECT",
|
||||
callback=self._on_car_model_click,
|
||||
),
|
||||
)
|
||||
|
||||
# Force Fingerprint Toggle
|
||||
self._force_fingerprint_item = ListItem(
|
||||
title="Disable Automatic Fingerprint Detection",
|
||||
description="<b>Force the selected fingerprint</b> and prevent it from ever changing.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("ForceFingerprint"),
|
||||
callback=lambda state: self._simple_toggle("ForceFingerprint", state),
|
||||
),
|
||||
)
|
||||
|
||||
# Disable openpilot Longitudinal Toggle
|
||||
self._disable_op_long_item = ListItem(
|
||||
title="Disable openpilot Longitudinal Control",
|
||||
description="<b>Disable openpilot longitudinal</b> and use the car's stock ACC instead.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("DisableOpenpilotLongitudinal"),
|
||||
callback=self._on_disable_op_long_toggle,
|
||||
),
|
||||
)
|
||||
|
||||
# GM Settings
|
||||
self._gm_control = FrogPilotManageControl(
|
||||
"GMToggles",
|
||||
"General Motors Settings",
|
||||
"<b>FrogPilot features for General Motors vehicles.</b>",
|
||||
"",
|
||||
)
|
||||
self._gm_control.set_manage_callback(self._open_gm_panel)
|
||||
|
||||
# HKG Settings
|
||||
self._hkg_control = FrogPilotManageControl(
|
||||
"HKGToggles",
|
||||
"Hyundai/Kia/Genesis Settings",
|
||||
"<b>FrogPilot features for Genesis, Hyundai, and Kia vehicles.</b>",
|
||||
"",
|
||||
)
|
||||
self._hkg_control.set_manage_callback(self._open_hkg_panel)
|
||||
|
||||
# Subaru Settings
|
||||
self._subaru_control = FrogPilotManageControl(
|
||||
"SubaruToggles",
|
||||
"Subaru Settings",
|
||||
"<b>FrogPilot features for Subaru vehicles.</b>",
|
||||
"",
|
||||
)
|
||||
self._subaru_control.set_manage_callback(self._open_subaru_panel)
|
||||
|
||||
# Toyota Settings
|
||||
self._toyota_control = FrogPilotManageControl(
|
||||
"ToyotaToggles",
|
||||
"Toyota/Lexus Settings",
|
||||
"<b>FrogPilot features for Lexus and Toyota vehicles.</b>",
|
||||
"",
|
||||
)
|
||||
self._toyota_control.set_manage_callback(self._open_toyota_panel)
|
||||
|
||||
# Vehicle Info
|
||||
self._vehicle_info_control = FrogPilotManageControl(
|
||||
"VehicleInfo",
|
||||
"Vehicle Info",
|
||||
"<b>Information about your vehicle in regards to openpilot support and functionality.</b>",
|
||||
"",
|
||||
)
|
||||
self._vehicle_info_control.set_manage_callback(self._open_vehicle_info_panel)
|
||||
|
||||
main_items = [
|
||||
self._car_make_item,
|
||||
self._car_model_item,
|
||||
self._force_fingerprint_item,
|
||||
self._disable_op_long_item,
|
||||
self._gm_control,
|
||||
self._hkg_control,
|
||||
self._subaru_control,
|
||||
self._toyota_control,
|
||||
self._vehicle_info_control,
|
||||
]
|
||||
|
||||
self._toggles["CarMake"] = self._car_make_item
|
||||
self._toggles["CarModel"] = self._car_model_item
|
||||
self._toggles["ForceFingerprint"] = self._force_fingerprint_item
|
||||
self._toggles["DisableOpenpilotLongitudinal"] = self._disable_op_long_item
|
||||
self._toggles["GMToggles"] = self._gm_control
|
||||
self._toggles["HKGToggles"] = self._hkg_control
|
||||
self._toggles["SubaruToggles"] = self._subaru_control
|
||||
self._toggles["ToyotaToggles"] = self._toyota_control
|
||||
self._toggles["VehicleInfo"] = self._vehicle_info_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_gm_panel(self):
|
||||
self._volt_sng_item = ListItem(
|
||||
title="Stop-and-Go Hack",
|
||||
description="<b>Force stop-and-go</b> on the 2017 Chevy Volt.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("VoltSNG"),
|
||||
callback=lambda state: self._simple_toggle("VoltSNG", state),
|
||||
),
|
||||
)
|
||||
|
||||
gm_items = [
|
||||
self._volt_sng_item,
|
||||
]
|
||||
|
||||
self._toggles["VoltSNG"] = self._volt_sng_item
|
||||
|
||||
self._gm_scroller = Scroller(gm_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_hkg_panel(self):
|
||||
self._taco_tune_item = ListItem(
|
||||
title="\"Taco Bell Run\" Torque Hack",
|
||||
description="<b>The steering torque hack from comma's 2022 \"Taco Bell Run\".</b> Designed to increase steering torque at low speeds for left and right turns.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("TacoTuneHacks"),
|
||||
callback=self._on_taco_tune_toggle,
|
||||
),
|
||||
)
|
||||
|
||||
hkg_items = [
|
||||
self._taco_tune_item,
|
||||
]
|
||||
|
||||
self._toggles["TacoTuneHacks"] = self._taco_tune_item
|
||||
|
||||
self._hkg_scroller = Scroller(hkg_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_subaru_panel(self):
|
||||
self._subaru_sng_item = ListItem(
|
||||
title="Stop and Go",
|
||||
description="<b>Stop and go for supported Subaru vehicles.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("SubaruSNG"),
|
||||
callback=lambda state: self._simple_toggle("SubaruSNG", state),
|
||||
),
|
||||
)
|
||||
|
||||
subaru_items = [
|
||||
self._subaru_sng_item,
|
||||
]
|
||||
|
||||
self._toggles["SubaruSNG"] = self._subaru_sng_item
|
||||
|
||||
self._subaru_scroller = Scroller(subaru_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_toyota_panel(self):
|
||||
# Toyota Doors with Lock/Unlock buttons
|
||||
self._toyota_doors_control = FrogPilotButtonToggleControl(
|
||||
"ToyotaDoors",
|
||||
"Automatically Lock/Unlock Doors",
|
||||
"<b>Automatically lock/unlock doors</b> when shifting in and out of drive.",
|
||||
"",
|
||||
button_params=["LockDoors", "UnlockDoors"],
|
||||
button_texts=["Lock", "Unlock"],
|
||||
)
|
||||
|
||||
# Cluster Offset with Reset button
|
||||
self._cluster_offset_control = FrogPilotParamValueButtonControl(
|
||||
"ClusterOffset",
|
||||
"Dashboard Speed Offset",
|
||||
"<b>The speed offset openpilot uses to match the speed on the dashboard display.</b>",
|
||||
"",
|
||||
min_value=1.000,
|
||||
max_value=1.050,
|
||||
label="x",
|
||||
interval=0.001,
|
||||
button_texts=["Reset"],
|
||||
)
|
||||
self._cluster_offset_control.set_button_click_callback(self._on_cluster_offset_reset)
|
||||
|
||||
# FrogsGoMoo's Tweaks
|
||||
self._frogs_go_moos_item = ListItem(
|
||||
title="FrogsGoMoo's Personal Tweaks",
|
||||
description="<b>Personal tweaks by FrogsGoMoo for quicker acceleration and smoother braking.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("FrogsGoMoosTweak"),
|
||||
callback=lambda state: self._simple_toggle("FrogsGoMoosTweak", state),
|
||||
),
|
||||
)
|
||||
|
||||
# Lock Doors Timer
|
||||
lock_timer_labels = build_lock_timer_labels()
|
||||
self._lock_doors_timer_control = FrogPilotParamValueControl(
|
||||
"LockDoorsTimer",
|
||||
"Lock Doors On Ignition Off After",
|
||||
"<b>Automatically lock the doors on ignition off</b> when no one is detected in the front seats.<br><br><b>Warning:</b> openpilot can't detect if keys are still inside the car, so ensure you have a spare key to prevent accidental lockouts!",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=300,
|
||||
value_labels=lock_timer_labels,
|
||||
interval=1,
|
||||
)
|
||||
|
||||
# SNG Hack
|
||||
self._sng_hack_item = ListItem(
|
||||
title="Stop-and-Go Hack",
|
||||
description="<b>Force stop-and-go</b> on Lexus/Toyota vehicles without stock stop-and-go functionality.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("SNGHack"),
|
||||
callback=lambda state: self._simple_toggle("SNGHack", state),
|
||||
),
|
||||
)
|
||||
|
||||
toyota_items = [
|
||||
self._toyota_doors_control,
|
||||
self._cluster_offset_control,
|
||||
self._frogs_go_moos_item,
|
||||
self._lock_doors_timer_control,
|
||||
self._sng_hack_item,
|
||||
]
|
||||
|
||||
self._toggles["ToyotaDoors"] = self._toyota_doors_control
|
||||
self._toggles["ClusterOffset"] = self._cluster_offset_control
|
||||
self._toggles["FrogsGoMoosTweak"] = self._frogs_go_moos_item
|
||||
self._toggles["LockDoorsTimer"] = self._lock_doors_timer_control
|
||||
self._toggles["SNGHack"] = self._sng_hack_item
|
||||
|
||||
self._toyota_scroller = Scroller(toyota_items, line_separator=True, spacing=0)
|
||||
|
||||
def _build_vehicle_info_panel(self):
|
||||
# All are read-only labels
|
||||
self._hardware_detected_item = ListItem(
|
||||
title="3rd Party Hardware Detected",
|
||||
description="<b>Detected 3rd party hardware.</b>",
|
||||
action_item=TextAction(lambda: self._get_hardware_detected(), color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
self._bsm_support_item = ListItem(
|
||||
title="Blind Spot Support",
|
||||
description="<b>Does openpilot use the vehicle's blind spot data?</b>",
|
||||
action_item=TextAction(lambda: "Yes" if self._has_bsm else "No", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
self._pedal_support_item = ListItem(
|
||||
title="comma Pedal Support",
|
||||
description="<b>Does your vehicle support the \"comma pedal\"?</b>",
|
||||
action_item=TextAction(lambda: "Yes" if self._can_use_pedal else "No", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
self._op_long_support_item = ListItem(
|
||||
title="openpilot Longitudinal Support",
|
||||
description="<b>Can openpilot control the vehicle's acceleration and braking?</b>",
|
||||
action_item=TextAction(lambda: "Yes" if self._has_openpilot_longitudinal else "No", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
self._radar_support_item = ListItem(
|
||||
title="Radar Support",
|
||||
description="<b>Does openpilot use the vehicle's radar data</b> alongside the device's camera for tracking lead vehicles?",
|
||||
action_item=TextAction(lambda: "Yes" if self._has_radar else "No", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
self._sdsu_support_item = ListItem(
|
||||
title="SDSU Support",
|
||||
description="<b>Does your vehicle support \"SDSUs\"?</b>",
|
||||
action_item=TextAction(lambda: "Yes" if self._can_use_sdsu else "No", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
self._sng_support_item = ListItem(
|
||||
title="Stop-and-Go Support",
|
||||
description="<b>Does your vehicle support stop-and-go driving?</b>",
|
||||
action_item=TextAction(lambda: "Yes" if self._has_sng else "No", color=ITEM_TEXT_VALUE_COLOR),
|
||||
)
|
||||
|
||||
vehicle_info_items = [
|
||||
self._hardware_detected_item,
|
||||
self._bsm_support_item,
|
||||
self._pedal_support_item,
|
||||
self._op_long_support_item,
|
||||
self._radar_support_item,
|
||||
self._sdsu_support_item,
|
||||
self._sng_support_item,
|
||||
]
|
||||
|
||||
self._toggles["HardwareDetected"] = self._hardware_detected_item
|
||||
self._toggles["BlindSpotSupport"] = self._bsm_support_item
|
||||
self._toggles["PedalSupport"] = self._pedal_support_item
|
||||
self._toggles["OpenpilotLongitudinal"] = self._op_long_support_item
|
||||
self._toggles["RadarSupport"] = self._radar_support_item
|
||||
self._toggles["SDSUSupport"] = self._sdsu_support_item
|
||||
self._toggles["SNGSupport"] = self._sng_support_item
|
||||
|
||||
self._vehicle_info_scroller = Scroller(vehicle_info_items, line_separator=True, spacing=0)
|
||||
|
||||
def _get_hardware_detected(self) -> str:
|
||||
"""Get comma-separated list of detected hardware."""
|
||||
detected = []
|
||||
if self._has_pedal:
|
||||
detected.append("comma Pedal")
|
||||
if self._has_sdsu:
|
||||
detected.append("SDSU")
|
||||
if self._has_zss:
|
||||
detected.append("ZSS")
|
||||
return ", ".join(detected) if detected else "None"
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _on_car_make_click(self):
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Choose your car make",
|
||||
CAR_MAKES,
|
||||
))
|
||||
# Note: Dialog result handling would set CarMake param
|
||||
|
||||
def _on_car_model_click(self):
|
||||
car_make = self._params.get("CarMake", encoding="utf-8") or ""
|
||||
if not car_make:
|
||||
gui_app.set_modal_overlay(alert_dialog("Please select a car make first."))
|
||||
return
|
||||
|
||||
car_names, self._car_models = get_car_names(car_make)
|
||||
|
||||
if not car_names:
|
||||
gui_app.set_modal_overlay(alert_dialog(f"No models found for {car_make}."))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Choose your car model",
|
||||
car_names,
|
||||
))
|
||||
# Note: Dialog result handling would set CarModel and CarModelName params
|
||||
|
||||
def _on_disable_op_long_toggle(self, state: bool):
|
||||
if state:
|
||||
def on_confirm():
|
||||
self._params.put_bool("DisableOpenpilotLongitudinal", True)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
if self._started:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Reboot required to take effect.",
|
||||
"Reboot Now",
|
||||
"Reboot Later",
|
||||
))
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Are you sure you want to completely disable openpilot longitudinal control?",
|
||||
"Yes",
|
||||
"No",
|
||||
))
|
||||
else:
|
||||
self._params.put_bool("DisableOpenpilotLongitudinal", False)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
self._update_toggles()
|
||||
|
||||
def _on_taco_tune_toggle(self, state: bool):
|
||||
self._params.put_bool("TacoTuneHacks", state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
if state and self._started:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
"Reboot required to take effect.",
|
||||
"Reboot Now",
|
||||
"Reboot Later",
|
||||
))
|
||||
|
||||
def _on_cluster_offset_reset(self, button_id: int):
|
||||
default_value = self._params.get_key_default_value("ClusterOffset")
|
||||
if default_value:
|
||||
try:
|
||||
self._params.put_float("ClusterOffset", float(default_value))
|
||||
except (ValueError, TypeError):
|
||||
self._params.put_float("ClusterOffset", 1.015)
|
||||
|
||||
def _open_gm_panel(self):
|
||||
self._current_panel = SubPanel.GM
|
||||
|
||||
def _open_hkg_panel(self):
|
||||
self._current_panel = SubPanel.HKG
|
||||
|
||||
def _open_subaru_panel(self):
|
||||
self._current_panel = SubPanel.SUBARU
|
||||
|
||||
def _open_toyota_panel(self):
|
||||
self._current_panel = SubPanel.TOYOTA
|
||||
|
||||
def _open_vehicle_info_panel(self):
|
||||
self._current_panel = SubPanel.VEHICLE_INFO
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _load_car_capabilities(self):
|
||||
"""Load car capabilities from frogpilot variables."""
|
||||
try:
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles
|
||||
toggles = get_frogpilot_toggles()
|
||||
|
||||
self._has_bsm = getattr(toggles, "has_bsm", False)
|
||||
self._has_openpilot_longitudinal = getattr(toggles, "has_openpilot_longitudinal", False)
|
||||
self._has_pedal = getattr(toggles, "has_pedal", False)
|
||||
self._has_radar = getattr(toggles, "has_radar", False)
|
||||
self._has_sdsu = getattr(toggles, "has_sdsu", False)
|
||||
self._has_sng = getattr(toggles, "has_sng", False)
|
||||
self._has_zss = getattr(toggles, "has_zss", False)
|
||||
self._can_use_pedal = getattr(toggles, "can_use_pedal", False)
|
||||
self._can_use_sdsu = getattr(toggles, "can_use_sdsu", False)
|
||||
self._has_alpha_longitudinal = getattr(toggles, "has_alpha_longitudinal", False)
|
||||
self._openpilot_longitudinal_disabled = getattr(toggles, "openpilot_longitudinal_disabled", False)
|
||||
|
||||
self._is_gm = getattr(toggles, "is_gm", False)
|
||||
self._is_hkg = getattr(toggles, "is_hkg", False)
|
||||
self._is_hkg_canfd = getattr(toggles, "is_hkg_canfd", False)
|
||||
self._is_subaru = getattr(toggles, "is_subaru", False)
|
||||
self._is_toyota = getattr(toggles, "is_toyota", False)
|
||||
self._is_volt = getattr(toggles, "is_volt", False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
|
||||
# GM panel visibility
|
||||
volt_sng_visible = self._is_gm and self._has_openpilot_longitudinal and self._is_volt and not self._has_sng
|
||||
if hasattr(self._volt_sng_item, "set_visible"):
|
||||
self._volt_sng_item.set_visible(volt_sng_visible)
|
||||
|
||||
# GM parent visible if any child is visible
|
||||
gm_visible = volt_sng_visible
|
||||
if hasattr(self._gm_control, "set_visible"):
|
||||
self._gm_control.set_visible(gm_visible)
|
||||
|
||||
# HKG panel visibility
|
||||
taco_tune_visible = self._is_hkg and self._is_hkg_canfd
|
||||
if hasattr(self._taco_tune_item, "set_visible"):
|
||||
self._taco_tune_item.set_visible(taco_tune_visible)
|
||||
|
||||
# HKG parent visible if any child is visible
|
||||
hkg_visible = taco_tune_visible
|
||||
if hasattr(self._hkg_control, "set_visible"):
|
||||
self._hkg_control.set_visible(hkg_visible)
|
||||
|
||||
# Subaru panel visibility
|
||||
subaru_sng_visible = self._is_subaru and self._has_sng
|
||||
if hasattr(self._subaru_sng_item, "set_visible"):
|
||||
self._subaru_sng_item.set_visible(subaru_sng_visible)
|
||||
|
||||
# Subaru parent visible if any child is visible
|
||||
subaru_visible = subaru_sng_visible
|
||||
if hasattr(self._subaru_control, "set_visible"):
|
||||
self._subaru_control.set_visible(subaru_visible)
|
||||
|
||||
# Toyota panel visibility
|
||||
toyota_doors_visible = self._is_toyota
|
||||
cluster_offset_visible = self._is_toyota
|
||||
frogs_go_moos_visible = self._is_toyota and self._has_openpilot_longitudinal
|
||||
lock_doors_timer_visible = self._is_toyota
|
||||
sng_hack_visible = self._is_toyota and self._has_openpilot_longitudinal and not self._has_sng
|
||||
|
||||
if hasattr(self._toyota_doors_control, "set_visible"):
|
||||
self._toyota_doors_control.set_visible(toyota_doors_visible)
|
||||
if hasattr(self._cluster_offset_control, "set_visible"):
|
||||
self._cluster_offset_control.set_visible(cluster_offset_visible)
|
||||
if hasattr(self._frogs_go_moos_item, "set_visible"):
|
||||
self._frogs_go_moos_item.set_visible(frogs_go_moos_visible)
|
||||
if hasattr(self._lock_doors_timer_control, "set_visible"):
|
||||
self._lock_doors_timer_control.set_visible(lock_doors_timer_visible)
|
||||
if hasattr(self._sng_hack_item, "set_visible"):
|
||||
self._sng_hack_item.set_visible(sng_hack_visible)
|
||||
|
||||
# Toyota parent visible if any child is visible
|
||||
toyota_visible = toyota_doors_visible or cluster_offset_visible or frogs_go_moos_visible or lock_doors_timer_visible or sng_hack_visible
|
||||
if hasattr(self._toyota_control, "set_visible"):
|
||||
self._toyota_control.set_visible(toyota_visible)
|
||||
|
||||
# Disable openpilot longitudinal visibility
|
||||
disable_op_long_visible = ((self._has_openpilot_longitudinal or self._openpilot_longitudinal_disabled) and
|
||||
not self._has_alpha_longitudinal)
|
||||
if hasattr(self._disable_op_long_item, "set_visible"):
|
||||
self._disable_op_long_item.set_visible(disable_op_long_visible)
|
||||
|
||||
def _update_car_display(self):
|
||||
"""Update car make/model display values."""
|
||||
car_make = self._params.get("CarMake", encoding="utf-8") or ""
|
||||
car_model_name = self._params.get("CarModelName", encoding="utf-8") or ""
|
||||
if not car_model_name:
|
||||
car_model_name = self._params.get("CarModel", encoding="utf-8") or ""
|
||||
|
||||
# Update display values if controls support it
|
||||
# Note: This would need the ListItem to support set_value
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._load_car_capabilities()
|
||||
self._update_toggles()
|
||||
self._update_car_display()
|
||||
self._started = ui_state.started
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
self._started = ui_state.started
|
||||
|
||||
if self._current_panel == SubPanel.GM:
|
||||
self._gm_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.HKG:
|
||||
self._hkg_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.SUBARU:
|
||||
self._subaru_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.TOYOTA:
|
||||
self._toyota_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.VEHICLE_INFO:
|
||||
self._vehicle_info_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,855 @@
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.ui.widgets.frogpilot_controls import (
|
||||
FrogPilotButtonsControl,
|
||||
FrogPilotButtonToggleControl,
|
||||
FrogPilotManageControl,
|
||||
FrogPilotParamValueControl,
|
||||
)
|
||||
|
||||
ADVANCED_CUSTOM_ONROAD_UI_KEYS = {
|
||||
"HideAlerts",
|
||||
"HideLeadMarker",
|
||||
"HideMaxSpeed",
|
||||
"HideSpeed",
|
||||
"HideSpeedLimit",
|
||||
"WheelSpeed",
|
||||
}
|
||||
|
||||
CUSTOM_ONROAD_UI_KEYS = {
|
||||
"AccelerationPath",
|
||||
"AdjacentPath",
|
||||
"BlindSpotPath",
|
||||
"Compass",
|
||||
"OnroadDistanceButton",
|
||||
"PedalsOnUI",
|
||||
"RotatingWheel",
|
||||
}
|
||||
|
||||
MODEL_UI_KEYS = {
|
||||
"DynamicPathWidth",
|
||||
"LaneLinesWidth",
|
||||
"PathEdgeWidth",
|
||||
"PathWidth",
|
||||
"RoadEdgesWidth",
|
||||
}
|
||||
|
||||
NAVIGATION_UI_KEYS = {
|
||||
"RoadNameUI",
|
||||
"ShowSpeedLimits",
|
||||
"SLCMapboxFiller",
|
||||
"UseVienna",
|
||||
}
|
||||
|
||||
QUALITY_OF_LIFE_KEYS = {
|
||||
"CameraView",
|
||||
"DriverCamera",
|
||||
"StoppedTimer",
|
||||
}
|
||||
|
||||
INCH_TO_CM = 2.54
|
||||
CM_TO_INCH = 1.0 / INCH_TO_CM
|
||||
FOOT_TO_METER = CV.FOOT_TO_METER
|
||||
METER_TO_FOOT = CV.METER_TO_FOOT
|
||||
|
||||
|
||||
class SubPanel(IntEnum):
|
||||
MAIN = 0
|
||||
ADVANCED_CUSTOM_UI = 1
|
||||
CUSTOM_UI = 2
|
||||
MODEL_UI = 3
|
||||
NAVIGATION_UI = 4
|
||||
QUALITY_OF_LIFE = 5
|
||||
|
||||
|
||||
def build_imperial_small_distance_labels():
|
||||
"""Build labels for inches (0-24)."""
|
||||
labels = {}
|
||||
for i in range(25):
|
||||
if i == 0:
|
||||
labels[i] = "Off"
|
||||
elif i == 1:
|
||||
labels[i] = "1 inch"
|
||||
else:
|
||||
labels[i] = f"{i} inches"
|
||||
return labels
|
||||
|
||||
|
||||
def build_metric_small_distance_labels():
|
||||
"""Build labels for centimeters (0-60)."""
|
||||
labels = {}
|
||||
for i in range(61):
|
||||
if i == 0:
|
||||
labels[i] = "Off"
|
||||
elif i == 1:
|
||||
labels[i] = "1 centimeter"
|
||||
else:
|
||||
labels[i] = f"{i} centimeters"
|
||||
return labels
|
||||
|
||||
|
||||
def build_imperial_distance_labels():
|
||||
"""Build labels for feet (0-10)."""
|
||||
labels = {}
|
||||
for i in range(11):
|
||||
if i == 0:
|
||||
labels[i] = "Off"
|
||||
elif i == 1:
|
||||
labels[i] = "1 foot"
|
||||
else:
|
||||
labels[i] = f"{i} feet"
|
||||
return labels
|
||||
|
||||
|
||||
def build_metric_distance_labels():
|
||||
"""Build labels for meters (0.0-3.0 in 0.1 steps)."""
|
||||
labels = {}
|
||||
for i in range(31):
|
||||
val = i / 10.0
|
||||
if val == 0.0:
|
||||
labels[val] = "Off"
|
||||
elif val == 1.0:
|
||||
labels[val] = "1 meter"
|
||||
else:
|
||||
labels[val] = f"{val:.1f} meters"
|
||||
return labels
|
||||
|
||||
|
||||
def build_path_edge_labels():
|
||||
"""Build labels for path edge width (0-100%)."""
|
||||
labels = {}
|
||||
for i in range(101):
|
||||
if i == 0:
|
||||
labels[i] = "Off"
|
||||
else:
|
||||
labels[i] = f"{i}%"
|
||||
return labels
|
||||
|
||||
|
||||
class FrogPilotVisualsPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = SubPanel.MAIN
|
||||
self._is_metric = False
|
||||
self._params = Params()
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# Car capabilities
|
||||
self._has_bsm = False
|
||||
self._has_openpilot_longitudinal = False
|
||||
|
||||
# Build all panels
|
||||
self._build_main_panel()
|
||||
self._build_advanced_custom_ui_panel()
|
||||
self._build_custom_ui_panel()
|
||||
self._build_model_ui_panel()
|
||||
self._build_navigation_ui_panel()
|
||||
self._build_quality_of_life_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._on_offroad_transition)
|
||||
|
||||
def _on_offroad_transition(self):
|
||||
previous_metric = self._is_metric
|
||||
self._is_metric = self._params.get_bool("IsMetric")
|
||||
if self._is_metric != previous_metric:
|
||||
self._convert_metric_values(previous_metric)
|
||||
self._update_metric()
|
||||
self._load_car_capabilities()
|
||||
self._update_toggles()
|
||||
|
||||
def _simple_toggle(self, param: str, state: bool):
|
||||
self._params.put_bool(param, state)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
# ==================== MAIN PANEL ====================
|
||||
def _build_main_panel(self):
|
||||
self._advanced_custom_ui_control = FrogPilotManageControl(
|
||||
"AdvancedCustomUI",
|
||||
"Advanced UI Controls",
|
||||
"<b>Advanced visual changes</b> to fine-tune how the driving screen looks.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_advanced_device.png",
|
||||
)
|
||||
self._advanced_custom_ui_control.set_manage_callback(self._open_advanced_custom_ui)
|
||||
|
||||
self._custom_ui_control = FrogPilotManageControl(
|
||||
"CustomUI",
|
||||
"Driving Screen Widgets",
|
||||
"<b>Custom FrogPilot widgets</b> for the driving screen.",
|
||||
"../assets/icons/calibration.png",
|
||||
)
|
||||
self._custom_ui_control.set_manage_callback(self._open_custom_ui)
|
||||
|
||||
self._model_ui_control = FrogPilotManageControl(
|
||||
"ModelUI",
|
||||
"Model UI",
|
||||
"<b>Model visualizations</b> for the driving path, lane lines, path edges, and road edges.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_road.png",
|
||||
)
|
||||
self._model_ui_control.set_manage_callback(self._open_model_ui)
|
||||
|
||||
self._navigation_ui_control = FrogPilotManageControl(
|
||||
"NavigationUI",
|
||||
"Navigation Widgets",
|
||||
"<b>Speed limits, and other navigation widgets.</b>",
|
||||
"../../frogpilot/assets/toggle_icons/icon_map.png",
|
||||
)
|
||||
self._navigation_ui_control.set_manage_callback(self._open_navigation_ui)
|
||||
|
||||
self._qol_visuals_control = FrogPilotManageControl(
|
||||
"QOLVisuals",
|
||||
"Quality of Life",
|
||||
"<b>Miscellaneous visual changes</b> to fine-tune how the driving screen looks.",
|
||||
"../../frogpilot/assets/toggle_icons/icon_quality_of_life.png",
|
||||
)
|
||||
self._qol_visuals_control.set_manage_callback(self._open_quality_of_life)
|
||||
|
||||
main_items = [
|
||||
self._advanced_custom_ui_control,
|
||||
self._custom_ui_control,
|
||||
self._model_ui_control,
|
||||
self._navigation_ui_control,
|
||||
self._qol_visuals_control,
|
||||
]
|
||||
|
||||
self._toggles["AdvancedCustomUI"] = self._advanced_custom_ui_control
|
||||
self._toggles["CustomUI"] = self._custom_ui_control
|
||||
self._toggles["ModelUI"] = self._model_ui_control
|
||||
self._toggles["NavigationUI"] = self._navigation_ui_control
|
||||
self._toggles["QOLVisuals"] = self._qol_visuals_control
|
||||
|
||||
self._main_scroller = Scroller(main_items, line_separator=True, spacing=0)
|
||||
|
||||
# ==================== ADVANCED CUSTOM UI PANEL ====================
|
||||
def _build_advanced_custom_ui_panel(self):
|
||||
self._hide_speed_item = ListItem(
|
||||
title="Hide Current Speed",
|
||||
description="<b>Hide the current speed</b> from the driving screen.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HideSpeed"),
|
||||
callback=lambda state: self._simple_toggle("HideSpeed", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._hide_lead_marker_item = ListItem(
|
||||
title="Hide Lead Marker",
|
||||
description="<b>Hide the lead-vehicle marker</b> from the driving screen.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HideLeadMarker"),
|
||||
callback=lambda state: self._simple_toggle("HideLeadMarker", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._hide_max_speed_item = ListItem(
|
||||
title="Hide Max Speed",
|
||||
description="<b>Hide the max speed</b> from the driving screen.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HideMaxSpeed"),
|
||||
callback=lambda state: self._simple_toggle("HideMaxSpeed", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._hide_alerts_item = ListItem(
|
||||
title="Hide Non-Critical Alerts",
|
||||
description="<b>Hide non-critical alerts</b> from the driving screen.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HideAlerts"),
|
||||
callback=lambda state: self._simple_toggle("HideAlerts", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._hide_speed_limit_item = ListItem(
|
||||
title="Hide Speed Limits",
|
||||
description="<b>Hide posted speed limits</b> from the driving screen.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("HideSpeedLimit"),
|
||||
callback=lambda state: self._simple_toggle("HideSpeedLimit", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._wheel_speed_item = ListItem(
|
||||
title="Use Wheel Speed",
|
||||
description="<b>Use the vehicle's wheel speed</b> instead of the cluster speed. This is purely a visual change and doesn't impact how openpilot drives!",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("WheelSpeed"),
|
||||
callback=lambda state: self._simple_toggle("WheelSpeed", state),
|
||||
),
|
||||
)
|
||||
|
||||
advanced_items = [
|
||||
self._hide_speed_item,
|
||||
self._hide_lead_marker_item,
|
||||
self._hide_max_speed_item,
|
||||
self._hide_alerts_item,
|
||||
self._hide_speed_limit_item,
|
||||
self._wheel_speed_item,
|
||||
]
|
||||
|
||||
self._toggles["HideSpeed"] = self._hide_speed_item
|
||||
self._toggles["HideLeadMarker"] = self._hide_lead_marker_item
|
||||
self._toggles["HideMaxSpeed"] = self._hide_max_speed_item
|
||||
self._toggles["HideAlerts"] = self._hide_alerts_item
|
||||
self._toggles["HideSpeedLimit"] = self._hide_speed_limit_item
|
||||
self._toggles["WheelSpeed"] = self._wheel_speed_item
|
||||
|
||||
self._advanced_custom_ui_scroller = Scroller(advanced_items, line_separator=True, spacing=0)
|
||||
|
||||
# ==================== CUSTOM UI PANEL ====================
|
||||
def _build_custom_ui_panel(self):
|
||||
self._acceleration_path_item = ListItem(
|
||||
title="Acceleration Path",
|
||||
description="<b>Color the driving path by planned acceleration and braking.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("AccelerationPath"),
|
||||
callback=lambda state: self._simple_toggle("AccelerationPath", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._adjacent_path_item = ListItem(
|
||||
title="Adjacent Lanes",
|
||||
description="<b>Show the driving paths for the left and right lanes.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("AdjacentPath"),
|
||||
callback=lambda state: self._simple_toggle("AdjacentPath", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._blind_spot_path_item = ListItem(
|
||||
title="Blind Spot Path",
|
||||
description="<b>Show a red path when a vehicle is in that lane's blind spot.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("BlindSpotPath"),
|
||||
callback=lambda state: self._simple_toggle("BlindSpotPath", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._compass_item = ListItem(
|
||||
title="Compass",
|
||||
description="<b>Show the current driving direction</b> with a simple on-screen compass.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("Compass"),
|
||||
callback=lambda state: self._simple_toggle("Compass", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._onroad_distance_button_item = ListItem(
|
||||
title="Driving Personality Button",
|
||||
description="<b>Control and view the current driving personality</b> via a driving screen widget.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("OnroadDistanceButton"),
|
||||
callback=lambda state: self._simple_toggle("OnroadDistanceButton", state),
|
||||
),
|
||||
)
|
||||
|
||||
# PedalsOnUI with Dynamic/Static mutually exclusive options
|
||||
self._pedals_on_ui_control = FrogPilotButtonToggleControl(
|
||||
"PedalsOnUI",
|
||||
"Gas / Brake Pedal Indicators",
|
||||
"<b>On-screen gas and brake indicators.</b><br><br><b>Dynamic</b>: Opacity changes according to how much openpilot is accelerating or braking<br><b>Static</b>: Full when active, dim when not",
|
||||
"",
|
||||
button_params=["DynamicPedalsOnUI", "StaticPedalsOnUI"],
|
||||
button_texts=["Dynamic", "Static"],
|
||||
)
|
||||
self._pedals_on_ui_control.set_button_callback(self._on_pedals_button_click)
|
||||
|
||||
self._rotating_wheel_item = ListItem(
|
||||
title="Rotating Steering Wheel",
|
||||
description="<b>Rotate the driving screen wheel</b> with the physical steering wheel.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("RotatingWheel"),
|
||||
callback=lambda state: self._simple_toggle("RotatingWheel", state),
|
||||
),
|
||||
)
|
||||
|
||||
custom_items = [
|
||||
self._acceleration_path_item,
|
||||
self._adjacent_path_item,
|
||||
self._blind_spot_path_item,
|
||||
self._compass_item,
|
||||
self._onroad_distance_button_item,
|
||||
self._pedals_on_ui_control,
|
||||
self._rotating_wheel_item,
|
||||
]
|
||||
|
||||
self._toggles["AccelerationPath"] = self._acceleration_path_item
|
||||
self._toggles["AdjacentPath"] = self._adjacent_path_item
|
||||
self._toggles["BlindSpotPath"] = self._blind_spot_path_item
|
||||
self._toggles["Compass"] = self._compass_item
|
||||
self._toggles["OnroadDistanceButton"] = self._onroad_distance_button_item
|
||||
self._toggles["PedalsOnUI"] = self._pedals_on_ui_control
|
||||
self._toggles["RotatingWheel"] = self._rotating_wheel_item
|
||||
|
||||
self._custom_ui_scroller = Scroller(custom_items, line_separator=True, spacing=0)
|
||||
|
||||
def _on_pedals_button_click(self, button_id: int):
|
||||
"""Handle mutually exclusive Dynamic/Static pedals options."""
|
||||
if button_id == 0:
|
||||
# Dynamic clicked - disable Static
|
||||
self._params.put_bool("StaticPedalsOnUI", False)
|
||||
elif button_id == 1:
|
||||
# Static clicked - disable Dynamic
|
||||
self._params.put_bool("DynamicPedalsOnUI", False)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
# ==================== MODEL UI PANEL ====================
|
||||
def _build_model_ui_panel(self):
|
||||
self._dynamic_path_width_item = ListItem(
|
||||
title="Dynamic Path Width",
|
||||
description="<b>Change the path width based on engagement.</b><br><br><b>Fully Engaged</b>: 100%<br><b>Always On Lateral</b>: 75%<br><b>Disengaged</b>: 50%",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("DynamicPathWidth"),
|
||||
callback=lambda state: self._simple_toggle("DynamicPathWidth", state),
|
||||
),
|
||||
)
|
||||
|
||||
# Lane Lines Width - 0-24 inches or 0-60 cm
|
||||
self._lane_lines_width_control = FrogPilotParamValueControl(
|
||||
"LaneLinesWidth",
|
||||
"Lane Lines Width",
|
||||
"<b>Set the lane-line thickness.</b><br><br>Default matches the MUTCD lane-line width standard of 4 inches.",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=24,
|
||||
unit=" inches",
|
||||
labels=build_imperial_small_distance_labels(),
|
||||
)
|
||||
|
||||
# Path Edge Width - 0-100%
|
||||
self._path_edge_width_control = FrogPilotParamValueControl(
|
||||
"PathEdgeWidth",
|
||||
"Path Edges Width",
|
||||
"<b>Set the driving-path edge width</b> that represents different driving modes and statuses.<br><br>Default is 20% of the total path width.<br><br>Color Guide:<br><br>- <b>Light Blue</b>: Always On Lateral<br>- <b>Green</b>: Default<br>- <b>Orange</b>: Experimental Mode<br>- <b>Red</b>: Traffic Mode<br>- <b>Yellow</b>: Conditional Experimental Mode overridden",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
unit="",
|
||||
labels=build_path_edge_labels(),
|
||||
)
|
||||
|
||||
# Path Width - 0-10 feet or 0-3 meters (0.1 step)
|
||||
self._path_width_control = FrogPilotParamValueControl(
|
||||
"PathWidth",
|
||||
"Path Width",
|
||||
"<b>Set the driving-path width.</b><br><br>Default (6.1 feet) matches the width of a 2019 Lexus ES 350.",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=10,
|
||||
unit=" feet",
|
||||
labels=build_imperial_distance_labels(),
|
||||
step=0.1,
|
||||
)
|
||||
|
||||
# Road Edges Width - 0-24 inches or 0-60 cm
|
||||
self._road_edges_width_control = FrogPilotParamValueControl(
|
||||
"RoadEdgesWidth",
|
||||
"Road Edges Width",
|
||||
"<b>Set the road-edge thickness.</b><br><br>Default matches half of the MUTCD lane-line width standard of 4 inches.",
|
||||
"",
|
||||
min_value=0,
|
||||
max_value=24,
|
||||
unit=" inches",
|
||||
labels=build_imperial_small_distance_labels(),
|
||||
)
|
||||
|
||||
model_items = [
|
||||
self._dynamic_path_width_item,
|
||||
self._lane_lines_width_control,
|
||||
self._path_edge_width_control,
|
||||
self._path_width_control,
|
||||
self._road_edges_width_control,
|
||||
]
|
||||
|
||||
self._toggles["DynamicPathWidth"] = self._dynamic_path_width_item
|
||||
self._toggles["LaneLinesWidth"] = self._lane_lines_width_control
|
||||
self._toggles["PathEdgeWidth"] = self._path_edge_width_control
|
||||
self._toggles["PathWidth"] = self._path_width_control
|
||||
self._toggles["RoadEdgesWidth"] = self._road_edges_width_control
|
||||
|
||||
self._model_ui_scroller = Scroller(model_items, line_separator=True, spacing=0)
|
||||
|
||||
# ==================== NAVIGATION UI PANEL ====================
|
||||
def _build_navigation_ui_panel(self):
|
||||
self._road_name_ui_item = ListItem(
|
||||
title="Road Name",
|
||||
description="<b>Display the road name at the bottom of the driving screen</b> using data from \"OpenStreetMap (OSM)\".",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("RoadNameUI"),
|
||||
callback=lambda state: self._simple_toggle("RoadNameUI", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._show_speed_limits_item = ListItem(
|
||||
title="Show Speed Limits",
|
||||
description="<b>Show speed limits</b> in the top-left corner of the driving screen. Uses data from the car's dashboard (if supported) and \"OpenStreetMap (OSM)\".",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("ShowSpeedLimits"),
|
||||
callback=lambda state: self._on_show_speed_limits_toggle(state),
|
||||
),
|
||||
)
|
||||
|
||||
self._slc_mapbox_filler_item = ListItem(
|
||||
title="Show Speed Limits from Mapbox",
|
||||
description="<b>Use Mapbox speed-limit data when no other source is available.</b>",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("SLCMapboxFiller"),
|
||||
callback=lambda state: self._simple_toggle("SLCMapboxFiller", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._use_vienna_item = ListItem(
|
||||
title="Use Vienna-Style Speed Signs",
|
||||
description="<b>Show Vienna-style (EU) speed-limit signs</b> instead of MUTCD (US).",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("UseVienna"),
|
||||
callback=lambda state: self._simple_toggle("UseVienna", state),
|
||||
),
|
||||
)
|
||||
|
||||
navigation_items = [
|
||||
self._road_name_ui_item,
|
||||
self._show_speed_limits_item,
|
||||
self._slc_mapbox_filler_item,
|
||||
self._use_vienna_item,
|
||||
]
|
||||
|
||||
self._toggles["RoadNameUI"] = self._road_name_ui_item
|
||||
self._toggles["ShowSpeedLimits"] = self._show_speed_limits_item
|
||||
self._toggles["SLCMapboxFiller"] = self._slc_mapbox_filler_item
|
||||
self._toggles["UseVienna"] = self._use_vienna_item
|
||||
|
||||
self._navigation_ui_scroller = Scroller(navigation_items, line_separator=True, spacing=0)
|
||||
|
||||
def _on_show_speed_limits_toggle(self, state: bool):
|
||||
"""Handle ShowSpeedLimits toggle and update dependent visibility."""
|
||||
self._params.put_bool("ShowSpeedLimits", state)
|
||||
update_frogpilot_toggles()
|
||||
self._update_toggles()
|
||||
|
||||
# ==================== QUALITY OF LIFE PANEL ====================
|
||||
def _build_quality_of_life_panel(self):
|
||||
# Camera View - 4 options: Auto, Driver, Standard, Wide
|
||||
self._camera_view_control = FrogPilotButtonsControl(
|
||||
"CameraView",
|
||||
"Camera View",
|
||||
"<b>Select the active camera view.</b> This is purely a visual change and doesn't impact how openpilot drives!",
|
||||
"",
|
||||
button_texts=["AUTO", "DRIVER", "STANDARD", "WIDE"],
|
||||
checkable=True,
|
||||
exclusive=True,
|
||||
)
|
||||
self._camera_view_control.set_click_callback(self._on_camera_view_click)
|
||||
self._update_camera_view_selection()
|
||||
|
||||
self._driver_camera_item = ListItem(
|
||||
title="Show Driver Camera When In Reverse",
|
||||
description="<b>Show the driver camera feed</b> when the vehicle is in reverse.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("DriverCamera"),
|
||||
callback=lambda state: self._simple_toggle("DriverCamera", state),
|
||||
),
|
||||
)
|
||||
|
||||
self._stopped_timer_item = ListItem(
|
||||
title="Stopped Timer",
|
||||
description="<b>Show a timer when stopped</b> in place of the current speed to indicate how long the vehicle has been stopped.",
|
||||
action_item=ToggleAction(
|
||||
initial_state=self._params.get_bool("StoppedTimer"),
|
||||
callback=lambda state: self._simple_toggle("StoppedTimer", state),
|
||||
),
|
||||
)
|
||||
|
||||
qol_items = [
|
||||
self._camera_view_control,
|
||||
self._driver_camera_item,
|
||||
self._stopped_timer_item,
|
||||
]
|
||||
|
||||
self._toggles["CameraView"] = self._camera_view_control
|
||||
self._toggles["DriverCamera"] = self._driver_camera_item
|
||||
self._toggles["StoppedTimer"] = self._stopped_timer_item
|
||||
|
||||
self._qol_scroller = Scroller(qol_items, line_separator=True, spacing=0)
|
||||
|
||||
def _on_camera_view_click(self, button_id: int):
|
||||
"""Handle camera view selection (0=Auto, 1=Driver, 2=Standard, 3=Wide)."""
|
||||
self._params.put_int("CameraView", button_id)
|
||||
self._update_camera_view_selection()
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _update_camera_view_selection(self):
|
||||
"""Update the camera view button selection state."""
|
||||
current = self._params.get_int("CameraView") or 0
|
||||
if hasattr(self._camera_view_control, "set_checked_button"):
|
||||
self._camera_view_control.set_checked_button(current)
|
||||
|
||||
# ==================== PANEL NAVIGATION ====================
|
||||
def _open_advanced_custom_ui(self):
|
||||
self._current_panel = SubPanel.ADVANCED_CUSTOM_UI
|
||||
|
||||
def _open_custom_ui(self):
|
||||
self._current_panel = SubPanel.CUSTOM_UI
|
||||
|
||||
def _open_model_ui(self):
|
||||
self._current_panel = SubPanel.MODEL_UI
|
||||
|
||||
def _open_navigation_ui(self):
|
||||
self._current_panel = SubPanel.NAVIGATION_UI
|
||||
|
||||
def _open_quality_of_life(self):
|
||||
self._current_panel = SubPanel.QUALITY_OF_LIFE
|
||||
|
||||
def _close_sub_panel(self):
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
# ==================== METRIC CONVERSION ====================
|
||||
def _convert_metric_values(self, was_metric: bool):
|
||||
"""Convert stored values when metric setting changes."""
|
||||
if was_metric:
|
||||
# Converting from metric to imperial
|
||||
small_conversion = CM_TO_INCH
|
||||
distance_conversion = METER_TO_FOOT
|
||||
else:
|
||||
# Converting from imperial to metric
|
||||
small_conversion = INCH_TO_CM
|
||||
distance_conversion = FOOT_TO_METER
|
||||
|
||||
# Convert lane lines width (inches <-> cm)
|
||||
lane_lines_width = self._params.get_int("LaneLinesWidth") or 0
|
||||
self._params.put_int("LaneLinesWidth", int(lane_lines_width * small_conversion))
|
||||
|
||||
# Convert road edges width (inches <-> cm)
|
||||
road_edges_width = self._params.get_int("RoadEdgesWidth") or 0
|
||||
self._params.put_int("RoadEdgesWidth", int(road_edges_width * small_conversion))
|
||||
|
||||
# Convert path width (feet <-> meters)
|
||||
path_width = self._params.get_float("PathWidth") or 0.0
|
||||
self._params.put_float("PathWidth", path_width * distance_conversion)
|
||||
|
||||
def _update_metric(self):
|
||||
"""Update control labels and ranges based on metric setting."""
|
||||
if self._is_metric:
|
||||
# Metric: cm for small distances, meters for path width
|
||||
self._lane_lines_width_control.set_description(
|
||||
"<b>Set the lane-line thickness.</b><br><br>Default matches the MUTCD lane-line width standard of 10 centimeters."
|
||||
)
|
||||
self._path_width_control.set_description(
|
||||
"<b>Set the driving-path width.</b><br><br>Default (1.9 meters) matches the width of a 2019 Lexus ES 350."
|
||||
)
|
||||
self._road_edges_width_control.set_description(
|
||||
"<b>Set the road-edge thickness.</b><br><br>Default matches half of the MUTCD lane-line width standard of 10 centimeters."
|
||||
)
|
||||
|
||||
if hasattr(self._lane_lines_width_control, "update_control"):
|
||||
self._lane_lines_width_control.update_control(0, 60, build_metric_small_distance_labels())
|
||||
if hasattr(self._road_edges_width_control, "update_control"):
|
||||
self._road_edges_width_control.update_control(0, 60, build_metric_small_distance_labels())
|
||||
if hasattr(self._path_width_control, "update_control"):
|
||||
self._path_width_control.update_control(0, 3, build_metric_distance_labels())
|
||||
else:
|
||||
# Imperial: inches for small distances, feet for path width
|
||||
self._lane_lines_width_control.set_description(
|
||||
"<b>Set the lane-line thickness.</b><br><br>Default matches the MUTCD lane-line width standard of 4 inches."
|
||||
)
|
||||
self._path_width_control.set_description(
|
||||
"<b>Set the driving-path width.</b><br><br>Default (6.1 feet) matches the width of a 2019 Lexus ES 350."
|
||||
)
|
||||
self._road_edges_width_control.set_description(
|
||||
"<b>Set the road-edge thickness.</b><br><br>Default matches half of the MUTCD lane-line width standard of 4 inches."
|
||||
)
|
||||
|
||||
if hasattr(self._lane_lines_width_control, "update_control"):
|
||||
self._lane_lines_width_control.update_control(0, 24, build_imperial_small_distance_labels())
|
||||
if hasattr(self._road_edges_width_control, "update_control"):
|
||||
self._road_edges_width_control.update_control(0, 24, build_imperial_small_distance_labels())
|
||||
if hasattr(self._path_width_control, "update_control"):
|
||||
self._path_width_control.update_control(0, 10, build_imperial_distance_labels())
|
||||
|
||||
# ==================== VISIBILITY UPDATES ====================
|
||||
def _load_car_capabilities(self):
|
||||
"""Load car capabilities from frogpilot variables."""
|
||||
try:
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles
|
||||
toggles = get_frogpilot_toggles()
|
||||
self._has_bsm = getattr(toggles, "has_bsm", False)
|
||||
self._has_openpilot_longitudinal = getattr(toggles, "has_openpilot_longitudinal", False)
|
||||
except Exception:
|
||||
self._has_bsm = False
|
||||
self._has_openpilot_longitudinal = False
|
||||
|
||||
def _update_toggles(self):
|
||||
"""Update toggle visibility based on tuning level and car capabilities."""
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
|
||||
# Load toggle levels
|
||||
try:
|
||||
import json
|
||||
toggle_levels_str = self._params.get("FrogPilotTogglesLevels", encoding="utf-8") or "{}"
|
||||
toggle_levels = json.loads(toggle_levels_str)
|
||||
except Exception:
|
||||
toggle_levels = {}
|
||||
|
||||
# First, hide all parent toggles
|
||||
for key in ["AdvancedCustomUI", "CustomUI", "ModelUI", "NavigationUI", "QOLVisuals"]:
|
||||
if key in self._toggles and hasattr(self._toggles[key], "set_visible"):
|
||||
self._toggles[key].set_visible(False)
|
||||
|
||||
# Check which child toggles are visible and show their parents accordingly
|
||||
slc_enabled = self._params.get_bool("SpeedLimitController")
|
||||
show_speed_limits = self._params.get_bool("ShowSpeedLimits")
|
||||
mapbox_key = self._params.get("MapboxSecretKey", encoding="utf-8") or ""
|
||||
|
||||
# Advanced Custom UI children
|
||||
advanced_visible = False
|
||||
for key in ADVANCED_CUSTOM_ONROAD_UI_KEYS:
|
||||
if key not in self._toggles:
|
||||
continue
|
||||
|
||||
toggle_level = toggle_levels.get(key, 0)
|
||||
visible = self._tuning_level >= toggle_level
|
||||
|
||||
# Special visibility conditions
|
||||
if key == "HideLeadMarker":
|
||||
visible = visible and self._has_openpilot_longitudinal
|
||||
elif key == "HideSpeedLimit":
|
||||
visible = visible and self._has_openpilot_longitudinal and slc_enabled
|
||||
|
||||
if hasattr(self._toggles[key], "set_visible"):
|
||||
self._toggles[key].set_visible(visible)
|
||||
|
||||
if visible:
|
||||
advanced_visible = True
|
||||
|
||||
if advanced_visible and hasattr(self._toggles["AdvancedCustomUI"], "set_visible"):
|
||||
self._toggles["AdvancedCustomUI"].set_visible(True)
|
||||
|
||||
# Custom UI children
|
||||
custom_visible = False
|
||||
for key in CUSTOM_ONROAD_UI_KEYS:
|
||||
if key not in self._toggles:
|
||||
continue
|
||||
|
||||
toggle_level = toggle_levels.get(key, 0)
|
||||
visible = self._tuning_level >= toggle_level
|
||||
|
||||
# Special visibility conditions
|
||||
if key == "AccelerationPath":
|
||||
visible = visible and self._has_openpilot_longitudinal
|
||||
elif key == "BlindSpotPath":
|
||||
visible = visible and self._has_bsm
|
||||
elif key == "OnroadDistanceButton":
|
||||
visible = visible and self._has_openpilot_longitudinal
|
||||
elif key == "PedalsOnUI":
|
||||
visible = visible and self._has_openpilot_longitudinal
|
||||
|
||||
if hasattr(self._toggles[key], "set_visible"):
|
||||
self._toggles[key].set_visible(visible)
|
||||
|
||||
if visible:
|
||||
custom_visible = True
|
||||
|
||||
if custom_visible and hasattr(self._toggles["CustomUI"], "set_visible"):
|
||||
self._toggles["CustomUI"].set_visible(True)
|
||||
|
||||
# Model UI children
|
||||
model_visible = False
|
||||
for key in MODEL_UI_KEYS:
|
||||
if key not in self._toggles:
|
||||
continue
|
||||
|
||||
toggle_level = toggle_levels.get(key, 0)
|
||||
visible = self._tuning_level >= toggle_level
|
||||
|
||||
if hasattr(self._toggles[key], "set_visible"):
|
||||
self._toggles[key].set_visible(visible)
|
||||
|
||||
if visible:
|
||||
model_visible = True
|
||||
|
||||
if model_visible and hasattr(self._toggles["ModelUI"], "set_visible"):
|
||||
self._toggles["ModelUI"].set_visible(True)
|
||||
|
||||
# Navigation UI children
|
||||
nav_visible = False
|
||||
for key in NAVIGATION_UI_KEYS:
|
||||
if key not in self._toggles:
|
||||
continue
|
||||
|
||||
toggle_level = toggle_levels.get(key, 0)
|
||||
visible = self._tuning_level >= toggle_level
|
||||
|
||||
# Special visibility conditions
|
||||
if key == "ShowSpeedLimits":
|
||||
# ShowSpeedLimits visible when SpeedLimitController is OFF or no longitudinal
|
||||
visible = visible and (not slc_enabled or not self._has_openpilot_longitudinal)
|
||||
elif key == "SLCMapboxFiller":
|
||||
# Visible if ShowSpeedLimits enabled, SLC off (or no longitudinal), and Mapbox key present
|
||||
visible = visible and show_speed_limits
|
||||
visible = visible and (not slc_enabled or not self._has_openpilot_longitudinal)
|
||||
visible = visible and bool(mapbox_key)
|
||||
elif key == "UseVienna":
|
||||
# Visible if either ShowSpeedLimits or SpeedLimitController is enabled
|
||||
visible = visible and (show_speed_limits or slc_enabled)
|
||||
|
||||
if hasattr(self._toggles[key], "set_visible"):
|
||||
self._toggles[key].set_visible(visible)
|
||||
|
||||
if visible:
|
||||
nav_visible = True
|
||||
|
||||
if nav_visible and hasattr(self._toggles["NavigationUI"], "set_visible"):
|
||||
self._toggles["NavigationUI"].set_visible(True)
|
||||
|
||||
# Quality of Life children
|
||||
qol_visible = False
|
||||
for key in QUALITY_OF_LIFE_KEYS:
|
||||
if key not in self._toggles:
|
||||
continue
|
||||
|
||||
toggle_level = toggle_levels.get(key, 0)
|
||||
visible = self._tuning_level >= toggle_level
|
||||
|
||||
if hasattr(self._toggles[key], "set_visible"):
|
||||
self._toggles[key].set_visible(visible)
|
||||
|
||||
if visible:
|
||||
qol_visible = True
|
||||
|
||||
if qol_visible and hasattr(self._toggles["QOLVisuals"], "set_visible"):
|
||||
self._toggles["QOLVisuals"].set_visible(True)
|
||||
|
||||
# ==================== LIFECYCLE ====================
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._main_scroller.show_event()
|
||||
self._is_metric = self._params.get_bool("IsMetric")
|
||||
self._update_metric()
|
||||
self._load_car_capabilities()
|
||||
self._update_toggles()
|
||||
self._update_camera_view_selection()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._current_panel = SubPanel.MAIN
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_panel == SubPanel.ADVANCED_CUSTOM_UI:
|
||||
self._advanced_custom_ui_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.CUSTOM_UI:
|
||||
self._custom_ui_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.MODEL_UI:
|
||||
self._model_ui_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.NAVIGATION_UI:
|
||||
self._navigation_ui_scroller.render(rect)
|
||||
elif self._current_panel == SubPanel.QUALITY_OF_LIFE:
|
||||
self._qol_scroller.render(rect)
|
||||
else:
|
||||
self._main_scroller.render(rect)
|
||||
@@ -0,0 +1,166 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import DialogResult
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ButtonAction, ITEM_TEXT_VALUE_COLOR, TextAction
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import update_frogpilot_toggles
|
||||
|
||||
# Button function mappings
|
||||
BUTTON_FUNCTIONS = {
|
||||
0: "No Action",
|
||||
3: "Pause Steering",
|
||||
}
|
||||
|
||||
LONGITUDINAL_FUNCTIONS = {
|
||||
1: "Change \"Personality Profile\"",
|
||||
2: "Force openpilot to Coast",
|
||||
4: "Pause Acceleration/Braking",
|
||||
5: "Toggle \"Experimental Mode\" On/Off",
|
||||
6: "Toggle \"Traffic Mode\" On/Off",
|
||||
}
|
||||
|
||||
# Button parameter configurations
|
||||
WHEEL_TOGGLES = [
|
||||
("DistanceButtonControl", "Distance Button", "<b>Action performed when the \"Distance\" button is pressed.</b>"),
|
||||
("LongDistanceButtonControl", "Distance Button (Long Press)", "<b>Action performed when the \"Distance\" button is pressed for more than 0.5 seconds.</b>"),
|
||||
("VeryLongDistanceButtonControl", "Distance Button (Very Long Press)", "<b>Action performed when the \"Distance\" button is pressed for more than 2.5 seconds.</b>"),
|
||||
("LKASButtonControl", "LKAS Button", "<b>Action performed when the \"LKAS\" button is pressed.</b>"),
|
||||
]
|
||||
|
||||
|
||||
class FrogPilotWheelPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._toggles = {}
|
||||
self._tuning_level = 0
|
||||
|
||||
# Car capabilities
|
||||
self._has_openpilot_longitudinal = False
|
||||
self._is_subaru = False
|
||||
self._lkas_allowed_for_aol = False
|
||||
|
||||
# Pending dialog action tracking
|
||||
self._pending_action = None # "select_function"
|
||||
self._pending_param = None
|
||||
|
||||
self._build_panel()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _build_panel(self):
|
||||
items = []
|
||||
|
||||
for param, title, desc in WHEEL_TOGGLES:
|
||||
control = ListItem(
|
||||
title=title,
|
||||
description=desc,
|
||||
action_item=ButtonAction(
|
||||
text="SELECT",
|
||||
callback=lambda p=param: self._on_button_click(p),
|
||||
),
|
||||
)
|
||||
|
||||
# Store reference for updating value display
|
||||
self._toggles[param] = control
|
||||
items.append(control)
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _get_function_name(self, param: str) -> str:
|
||||
"""Get the display name for the currently selected function."""
|
||||
value = self._params.get_int(param)
|
||||
|
||||
# Check both base and longitudinal functions
|
||||
all_functions = {**BUTTON_FUNCTIONS}
|
||||
if self._has_openpilot_longitudinal:
|
||||
all_functions.update(LONGITUDINAL_FUNCTIONS)
|
||||
|
||||
return all_functions.get(value, "No Action")
|
||||
|
||||
def _on_button_click(self, param: str):
|
||||
"""Handle button click to open function selection dialog."""
|
||||
# Build available functions list
|
||||
functions = dict(BUTTON_FUNCTIONS)
|
||||
if self._has_openpilot_longitudinal:
|
||||
functions.update(LONGITUDINAL_FUNCTIONS)
|
||||
|
||||
# Get current selection
|
||||
current_value = self._params.get_int(param)
|
||||
current_name = functions.get(current_value, "No Action")
|
||||
|
||||
# Show selection dialog
|
||||
self._pending_action = "select_function"
|
||||
self._pending_param = param
|
||||
gui_app.set_modal_overlay(MultiOptionDialog(
|
||||
"Select a function to assign to this button",
|
||||
list(functions.values()),
|
||||
current_name,
|
||||
))
|
||||
|
||||
def handle_dialog_result(self, result: DialogResult, selection: str = ""):
|
||||
"""Handle dialog results for pending actions."""
|
||||
action = self._pending_action
|
||||
param = self._pending_param
|
||||
self._pending_action = None
|
||||
self._pending_param = None
|
||||
|
||||
if action == "select_function":
|
||||
if result != DialogResult.CONFIRM or not selection:
|
||||
return
|
||||
|
||||
# Find the function ID for the selected name
|
||||
all_functions = {**BUTTON_FUNCTIONS, **LONGITUDINAL_FUNCTIONS}
|
||||
function_id = None
|
||||
for fid, name in all_functions.items():
|
||||
if name == selection:
|
||||
function_id = fid
|
||||
break
|
||||
|
||||
if function_id is not None and param:
|
||||
self._params.put_int(param, function_id)
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def _load_car_capabilities(self):
|
||||
"""Load car capabilities from frogpilot variables."""
|
||||
try:
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles
|
||||
toggles = get_frogpilot_toggles()
|
||||
|
||||
self._has_openpilot_longitudinal = getattr(toggles, "has_openpilot_longitudinal", False)
|
||||
self._is_subaru = getattr(toggles, "is_subaru", False)
|
||||
self._lkas_allowed_for_aol = getattr(toggles, "lkas_allowed_for_aol", False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _update_toggles(self):
|
||||
self._tuning_level = self._params.get_int("TuningLevel") or 0
|
||||
self._load_car_capabilities()
|
||||
|
||||
# LKAS button visibility
|
||||
lkas_visible = True
|
||||
if self._is_subaru:
|
||||
lkas_visible = False
|
||||
elif self._lkas_allowed_for_aol:
|
||||
aol_enabled = self._params.get_bool("AlwaysOnLateral")
|
||||
aol_lkas = self._params.get_bool("AlwaysOnLateralLKAS")
|
||||
if aol_enabled and aol_lkas:
|
||||
lkas_visible = False
|
||||
|
||||
lkas_control = self._toggles.get("LKASButtonControl")
|
||||
if lkas_control and hasattr(lkas_control, "set_visible"):
|
||||
lkas_control.set_visible(lkas_visible)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._load_car_capabilities()
|
||||
self._update_toggles()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
@@ -0,0 +1,3 @@
|
||||
from openpilot.frogpilot.ui.layouts.settings.frogpilot import FrogPilotLayout
|
||||
|
||||
__all__ = ["FrogPilotLayout"]
|
||||
@@ -5,6 +5,7 @@ from collections.abc import Callable
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.frogpilot import FrogPilotLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
@@ -37,6 +38,7 @@ class PanelType(IntEnum):
|
||||
SOFTWARE = 3
|
||||
FIREHOSE = 4
|
||||
DEVELOPER = 5
|
||||
FROGPILOT = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -62,6 +64,7 @@ class SettingsLayout(Widget):
|
||||
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
|
||||
PanelType.FROGPILOT: PanelInfo(tr_noop("FrogPilot"), FrogPilotLayout()),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
Reference in New Issue
Block a user