diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 813079d90..153383318 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -1,7 +1,5 @@ import os -import threading import pyray as rl -from enum import IntEnum from pathlib import Path from collections.abc import Callable @@ -126,12 +124,6 @@ class DeviceInfoLayoutMici(Widget): self._serial_number_text_label.render() -class UpdaterState(IntEnum): - IDLE = 0 - WAITING_FOR_UPDATER = 1 - UPDATER_RESPONDING = 2 - - def _konik_toggle_path() -> Path: return Path(Paths.comma_home()) / "starpilot" / "cache" / "use_konik" if PC else Path("/cache/use_konik") @@ -225,127 +217,6 @@ class PairBigButton(BigButton): gui_app.push_widget(dlg) -UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond - - -class UpdateOpenpilotBigButton(BigButton): - def __init__(self): - self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) - self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70) - self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) - super().__init__("update openpilot", "", self._txt_update_icon) - - self._waiting_for_updater_t: float | None = None - self._hide_value_t: float | None = None - self._state: UpdaterState = UpdaterState.IDLE - - ui_state.add_offroad_transition_callback(self.offroad_transition) - - def offroad_transition(self): - if ui_state.is_offroad(): - self.set_enabled(True) - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - if not system_time_valid(): - dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) - gui_app.push_widget(dlg) - return - - self.set_enabled(False) - self._state = UpdaterState.WAITING_FOR_UPDATER - self.set_icon(self._txt_update_icon) - - def run(): - if self.get_value() == "download update": - os.system("pkill -SIGHUP -f system.updated.updated") - elif self.get_value() == "update now": - ui_state.params.put_bool("DoReboot", True) - else: - os.system("pkill -SIGUSR1 -f system.updated.updated") - - threading.Thread(target=run, daemon=True).start() - - def set_value(self, value: str): - super().set_value(value) - if value: - self.set_text("") - else: - self.set_text("update openpilot") - - def _update_state(self): - super()._update_state() - - if ui_state.started: - self.set_enabled(False) - return - - updater_state = ui_state.params.get("UpdaterState") or "" - failed_count = ui_state.params.get("UpdateFailedCount") - failed = False if failed_count is None else int(failed_count) > 0 - - if ui_state.params.get_bool("UpdateAvailable"): - self.set_rotate_icon(False) - self.set_enabled(True) - if self.get_value() != "update now": - self.set_value("update now") - self.set_icon(self._txt_reboot_icon) - - elif self._state == UpdaterState.WAITING_FOR_UPDATER: - self.set_rotate_icon(True) - if updater_state != "idle": - self._state = UpdaterState.UPDATER_RESPONDING - - # Recover from updater not responding (time invalid shortly after boot) - if self._waiting_for_updater_t is None: - self._waiting_for_updater_t = rl.get_time() - - if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT: - self.set_rotate_icon(False) - self.set_value("updater failed\nto respond") - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - - elif self._state == UpdaterState.UPDATER_RESPONDING: - if updater_state == "idle": - self.set_rotate_icon(False) - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - else: - if self.get_value() != updater_state: - self.set_value(updater_state) - - elif self._state == UpdaterState.IDLE: - self.set_rotate_icon(False) - if failed: - if self.get_value() != "failed to update": - self.set_value("failed to update") - - elif ui_state.params.get_bool("UpdaterFetchAvailable"): - self.set_enabled(True) - if self.get_value() != "download update": - self.set_value("download update") - - elif self._hide_value_t is not None: - self.set_enabled(True) - if self.get_value() == "checking...": - self.set_value("up to date") - self.set_icon(self._txt_up_to_date_icon) - - # Hide previous text after short amount of time (up to date or failed) - if rl.get_time() - self._hide_value_t > 3.0: - self._hide_value_t = None - self.set_value("") - self.set_icon(self._txt_update_icon) - else: - if self.get_value() != "": - self.set_value("") - - if self._state != UpdaterState.WAITING_FOR_UPDATER: - self._waiting_for_updater_t = None - - class DeviceLayoutMici(NavScroller): def __init__(self): super().__init__() @@ -374,9 +245,6 @@ class DeviceLayoutMici(NavScroller): params.remove("IsRHDOverride") params.put_bool("OnroadCycleRequested", True) - def uninstall_openpilot_callback(): - ui_state.params.put_bool("DoUninstall", True) - reset_driver_monitoring_btn = EngagedConfirmationButton( "reset driver\nmonitoring", "reset driver monitoring", @@ -387,10 +255,6 @@ class DeviceLayoutMici(NavScroller): reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64), reset_calibration_callback) - uninstall_openpilot_btn = EngagedConfirmationButton("uninstall openpilot", "uninstall", - gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), - uninstall_openpilot_callback, exit_on_confirm=False) - reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70), reboot_callback, exit_on_confirm=False) @@ -417,7 +281,6 @@ class DeviceLayoutMici(NavScroller): self._scroller.add_widgets([ DeviceInfoLayoutMici(), - UpdateOpenpilotBigButton(), self._connect_server_btn, PairBigButton(), self._simple_mode_btn, @@ -427,7 +290,6 @@ class DeviceLayoutMici(NavScroller): terms_btn, regulatory_btn, reset_calibration_btn, - uninstall_openpilot_btn, reboot_btn, self._power_off_btn, ]) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 6901e3740..35307b4dd 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -6,6 +6,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import from openpilot.selfdrive.ui.mici.layouts.settings.vehicle import VehicleLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici +from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.driving_model import DrivingModelBigButton from openpilot.selfdrive.ui.mici.layouts.settings.galaxy import GalaxyBigButton from openpilot.selfdrive.ui.mici.layouts.settings.visuals import VisualsLayoutMici @@ -78,6 +79,10 @@ class SettingsLayout(NavScroller): device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + software_panel = SoftwareLayoutMici() + software_btn = SettingsBigButton("software", "", gui_app.texture("icons_mici/settings/device/update.png", 64, 75)) + software_btn.set_click_callback(lambda: gui_app.push_widget(software_panel)) + developer_panel = DeveloperLayoutMici() developer_btn = SettingsBigButton("developer", "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60)) developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) @@ -92,6 +97,7 @@ class SettingsLayout(NavScroller): self._force_drive_state_btn, vehicle_btn, device_btn, + software_btn, self._driving_model_btn, visuals_btn, galaxy_btn, diff --git a/selfdrive/ui/mici/layouts/settings/software.py b/selfdrive/ui/mici/layouts/settings/software.py new file mode 100644 index 000000000..34c82c4a9 --- /dev/null +++ b/selfdrive/ui/mici/layouts/settings/software.py @@ -0,0 +1,307 @@ +import os +import threading +from collections.abc import Callable +from enum import IntEnum + +import pyray as rl + +from openpilot.common.time_helpers import system_time_valid +from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl +from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.starpilot.common.starpilot_utilities import is_FrogsGoMoo +from openpilot.system.ui.lib.application import FontWeight, MousePos, gui_app +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.widgets.scroller import NavScroller + +UPDATER_TIMEOUT = 10.0 + + +def _split_description(desc: str) -> tuple[str, str, str, str] | None: + parts = [p.strip() for p in desc.split(" / ")] + if len(parts) != 4: + return None + version, branch, commit, date = parts + return version, branch, commit, date + + +def _update_failed() -> bool: + failed_count = ui_state.params.get("UpdateFailedCount") + try: + return False if failed_count is None else int(failed_count) > 0 + except (TypeError, ValueError): + return False + + +def _request_update_check(): + ui_state.params_memory.put_bool("ManualUpdateInitiated", True) + os.system("pkill -SIGUSR1 -f system.updated.updated") + + +def _request_update_download(): + ui_state.params_memory.put_bool("ManualUpdateInitiated", True) + os.system("pkill -SIGHUP -f system.updated.updated") + + +class UpdaterState(IntEnum): + IDLE = 0 + WAITING_FOR_UPDATER = 1 + UPDATER_RESPONDING = 2 + + +class SoftwareInfoLayoutMici(Widget): + def __init__(self): + super().__init__() + + self.set_rect(rl.Rectangle(0, 0, 360, 180)) + + subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) + max_width = int(self._rect.width - 20) + self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False) + + self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False) + + def _update_state(self): + desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") + if desc is not None: + version, branch, commit, date = desc + self._version_text_label.set_text(f"{version} ({date})") + self._branch_text_label.set_text(f"{branch} ({commit})") + else: + self._version_text_label.set_text(ui_state.params.get("Version") or "N/A") + self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A") + + def _render(self, _): + self._version_label.set_position(self._rect.x + 20, self._rect.y - 10) + self._version_label.render() + + self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) + self._version_text_label.render() + + self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) + self._branch_label.render() + + self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) + self._branch_text_label.render() + + +class CheckUpdateButton(BigButton): + def __init__(self): + self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) + super().__init__("check for update", "", self._txt_update_icon) + + self._waiting_for_updater_t: float | None = None + self._hide_value_t: float | None = None + self._state: UpdaterState = UpdaterState.IDLE + + ui_state.add_offroad_transition_callback(self.offroad_transition) + + def offroad_transition(self): + if ui_state.is_offroad(): + self.set_enabled(True) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + + if not system_time_valid(): + dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) + gui_app.push_widget(dlg) + return + + self.set_enabled(False) + self._state = UpdaterState.WAITING_FOR_UPDATER + self.set_icon(self._txt_update_icon) + + def run(): + if self.get_value() == "download update": + _request_update_download() + else: + _request_update_check() + + threading.Thread(target=run, daemon=True).start() + + def set_value(self, value: str): + super().set_value(value) + self.set_text("" if value else "check for update") + + def _update_state(self): + super()._update_state() + + if ui_state.started: + self.set_enabled(False) + return + + updater_state = ui_state.params.get("UpdaterState") or "" + + if self._state == UpdaterState.WAITING_FOR_UPDATER: + self.set_rotate_icon(True) + if updater_state != "idle": + self._state = UpdaterState.UPDATER_RESPONDING + + if self._waiting_for_updater_t is None: + self._waiting_for_updater_t = rl.get_time() + + if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT: + self.set_rotate_icon(False) + self.set_value("updater failed\nto respond") + self._state = UpdaterState.IDLE + self._hide_value_t = rl.get_time() + + elif self._state == UpdaterState.UPDATER_RESPONDING: + if updater_state == "idle": + self.set_rotate_icon(False) + self._state = UpdaterState.IDLE + self._hide_value_t = rl.get_time() + elif self.get_value() != updater_state: + self.set_value(updater_state) + + elif self._state == UpdaterState.IDLE: + self.set_rotate_icon(False) + if _update_failed(): + self.set_enabled(True) + if self.get_value() != "failed to update": + self.set_value("failed to update") + + elif ui_state.params.get_bool("UpdaterFetchAvailable"): + self.set_enabled(True) + if self.get_value() != "download update": + self.set_value("download update") + + elif self._hide_value_t is not None: + self.set_enabled(True) + if self.get_value() == "checking...": + self.set_value("up to date") + self.set_icon(self._txt_up_to_date_icon) + + if rl.get_time() - self._hide_value_t > 3.0: + self._hide_value_t = None + self.set_value("") + self.set_icon(self._txt_update_icon) + elif self.get_value() != "": + self.set_value("") + + if self._state != UpdaterState.WAITING_FOR_UPDATER: + self._waiting_for_updater_t = None + + +class InstallUpdateButton(BigButton): + def __init__(self): + super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) + self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) + + def _update_state(self): + super()._update_state() + + desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "") + value = f"{desc[0]} ({desc[1]})" if desc is not None else "" + if self.get_value() != value: + self.set_value(value) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + + self.set_enabled(False) + + def run(): + ui_state.params.put_bool("DoReboot", True) + + threading.Thread(target=run, daemon=True).start() + + +class BranchSelectPage(NavScroller): + def __init__(self, on_select: Callable[[str], None]): + super().__init__() + + params = ui_state.params + current_git_branch = params.get("GitBranch") or "" + branches_str = params.get("UpdaterAvailableBranches") or "" + branches = [b for b in branches_str.split(",") if b] + + if not is_FrogsGoMoo(): + for hidden_branch in ("StarPilot-Vetting", "MAKE-PRS-HERE"): + if hidden_branch in branches: + branches.remove(hidden_branch) + + for branch in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]: + if branch in branches: + branches.remove(branch) + branches.insert(0, branch) + + current_target = params.get("UpdaterTargetBranch") or "" + check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) + + buttons = [] + if not branches: + btn = BigButton("no branches", "check for update first", scroll=True) + btn.set_enabled(False) + buttons.append(btn) + else: + for branch in branches: + btn = BigButton(branch, "", check_icon if branch == current_target else None, scroll=True) + btn.set_click_callback(lambda b=branch: self.dismiss(lambda: on_select(b))) + buttons.append(btn) + + self._scroller.add_widgets(buttons) + + +class TargetBranchButton(BigButton): + def __init__(self): + super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "") + self._download_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + self.set_click_callback(self._on_click) + self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) + self.set_enabled(lambda: ui_state.is_offroad()) + + def _update_state(self): + super()._update_state() + + target = ui_state.params.get("UpdaterTargetBranch") or "" + if self.get_value() != target: + self.set_value(target) + + def _on_click(self): + gui_app.push_widget(BranchSelectPage(self._on_select)) + + def _on_select(self, branch: str): + previous = ui_state.params.get("UpdaterTargetBranch") or "" + ui_state.params.put("UpdaterTargetBranch", branch) + self.set_value(branch) + _request_update_check() + + if branch != previous: + gui_app.push_widget(BigConfirmationDialog("slide to\ndownload", self._download_icon, _request_update_download)) + + +class SoftwareLayoutMici(NavScroller): + def __init__(self): + super().__init__() + + def uninstall_openpilot_callback(): + ui_state.params.put_bool("DoUninstall", True) + + self._automatic_updates_btn = BigParamControl("auto updates", "AutomaticUpdates") + + uninstall_openpilot_btn = EngagedConfirmationButton("uninstall\nStarPilot", "uninstall", + gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), + uninstall_openpilot_callback, exit_on_confirm=False) + + self._scroller.add_widgets([ + SoftwareInfoLayoutMici(), + CheckUpdateButton(), + InstallUpdateButton(), + TargetBranchButton(), + self._automatic_updates_btn, + uninstall_openpilot_btn, + ]) + + def _update_state(self): + super()._update_state() + self._automatic_updates_btn.refresh()