diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index f42682e2f..94598ab14 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -58,6 +58,7 @@ class SoftwareLayout(Widget): self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off.")) self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "") self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update) + self._force_download_btn = button_item(lambda: tr("Force Download"), lambda: tr("FORCE"), callback=self._on_force_download) # Install button is initially hidden self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update) @@ -76,6 +77,7 @@ class SoftwareLayout(Widget): self._onroad_label, self._version_item, self._download_btn, + self._force_download_btn, self._install_btn, self._branch_btn, button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall), @@ -100,6 +102,7 @@ class SoftwareLayout(Widget): # Update download button visibility and state self._download_btn.set_visible(ui_state.is_offroad()) + self._force_download_btn.set_visible(ui_state.is_offroad()) updater_state = ui_state.params.get("UpdaterState") or "idle" failed_count = ui_state.params.get("UpdateFailedCount") or 0 @@ -110,6 +113,7 @@ class SoftwareLayout(Widget): # Updater responded self._waiting_for_updater = False self._download_btn.action_item.set_enabled(False) + self._force_download_btn.action_item.set_enabled(False) # Use the mapping, with a fallback to the original state string display_text = STATE_TO_DISPLAY_TEXT.get(updater_state, updater_state) self._download_btn.action_item.set_value(display_text) @@ -135,6 +139,7 @@ class SoftwareLayout(Widget): # Only enable if we're not waiting for updater to flip out of idle self._download_btn.action_item.set_enabled(not self._waiting_for_updater) + self._force_download_btn.action_item.set_enabled(not self._waiting_for_updater) # Update target branch button value current_branch = ui_state.params.get("UpdaterTargetBranch") or "" @@ -180,6 +185,12 @@ class SoftwareLayout(Widget): self._install_btn.action_item.set_enabled(False) ui_state.params.put_bool("DoReboot", True) + def _on_force_download(self): + self._force_download_btn.action_item.set_enabled(False) + self._waiting_for_updater = True + self._waiting_start_ts = time.monotonic() + os.system("pkill -SIGUSR2 -f system.updated.updated") + def _on_select_branch(self): # Get available branches and order current_git_branch = ui_state.params.get("GitBranch") or "" diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index e0d89a541..1844b356d 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -15,7 +15,8 @@ from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCamera from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer @@ -167,6 +168,42 @@ class PairBigButton(BigButton): UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond +class TargetBranchBigButton(BigButton): + def __init__(self): + super().__init__("target branch", "", gui_app.texture("icons_mici/settings/device/update.png", 64, 75), scroll=True) + self._branch_dialog: MultiOptionDialog | None = None + self.set_click_callback(self._on_select_branch) + + def _on_select_branch(self): + current_git_branch = ui_state.params.get("GitBranch") or "" + branches_str = ui_state.params.get("UpdaterAvailableBranches") or "" + branches = [b for b in branches_str.split(",") if b] + + for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]: + if b in branches: + branches.remove(b) + branches.insert(0, b) + + current_target = ui_state.params.get("UpdaterTargetBranch") or "" + + def handle_selection(result: DialogResult): + if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection: + selection = self._branch_dialog.selection + ui_state.params.put("UpdaterTargetBranch", selection) + os.system("pkill -SIGUSR1 -f system.updated.updated") + self.set_value(selection) + self._branch_dialog = None + + self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target, callback=handle_selection) + gui_app.push_widget(self._branch_dialog) + + def _update_state(self): + super()._update_state() + current_branch = ui_state.params.get("UpdaterTargetBranch") or ui_state.params.get("GitBranch") or "" + if self.get_value() != current_branch: + self.set_value(current_branch) + + class UpdateOpenpilotBigButton(BigButton): def __init__(self): self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) @@ -285,6 +322,84 @@ class UpdateOpenpilotBigButton(BigButton): self._waiting_for_updater_t = None +class ForceDownloadBigButton(BigButton): + def __init__(self): + self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + super().__init__("force download", "", 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(): + os.system("pkill -SIGUSR2 -f system.updated.updated") + + threading.Thread(target=run, daemon=True).start() + + 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) + self.set_enabled(True) + + if self._hide_value_t is not None: + if rl.get_time() - self._hide_value_t > 3.0: + self._hide_value_t = None + self.set_value("") + elif 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__() @@ -339,7 +454,9 @@ class DeviceLayoutMici(NavScroller): self._scroller.add_widgets([ DeviceInfoLayoutMici(), + TargetBranchBigButton(), UpdateOpenpilotBigButton(), + ForceDownloadBigButton(), PairBigButton(), review_training_guide_btn, driver_cam_btn, diff --git a/system/updated/updated.py b/system/updated/updated.py index c10a7097c..517f6d61d 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -42,6 +42,7 @@ class UserRequest: NONE = 0 CHECK = 1 FETCH = 2 + FORCE_FETCH = 3 class WaitTimeHelper: def __init__(self): @@ -49,6 +50,7 @@ class WaitTimeHelper: self.user_request = UserRequest.NONE signal.signal(signal.SIGHUP, self.update_now) signal.signal(signal.SIGUSR1, self.check_now) + signal.signal(signal.SIGUSR2, self.force_update_now) def update_now(self, signum: int, frame) -> None: cloudlog.info("caught SIGHUP, attempting to downloading update") @@ -60,6 +62,11 @@ class WaitTimeHelper: self.user_request = UserRequest.CHECK self.ready_event.set() + def force_update_now(self, signum: int, frame) -> None: + cloudlog.info("caught SIGUSR2, attempting forced update download") + self.user_request = UserRequest.FORCE_FETCH + self.ready_event.set() + def sleep(self, t: float) -> None: self.ready_event.wait(timeout=t) @@ -359,7 +366,7 @@ class Updater: else: cloudlog.info(f"up to date on {cur_branch} ({str(cur_commit)[:7]})") - def fetch_update(self) -> None: + def fetch_update(self, force: bool = False) -> None: cloudlog.info("attempting git fetch inside staging overlay") self.params.put("UpdaterState", "downloading...") @@ -373,7 +380,10 @@ class Updater: run(["git", "config", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"], OVERLAY_MERGED) branch = self.target_branch - git_fetch_output = run(["git", "fetch", "origin", branch], OVERLAY_MERGED) + fetch_cmd = ["git", "fetch", "origin", branch] + if force: + fetch_cmd = ["git", "fetch", "--prune", "origin", branch] + git_fetch_output = run(fetch_cmd, OVERLAY_MERGED) cloudlog.info("git fetch success: %s", git_fetch_output) cloudlog.info("git reset in progress") @@ -382,10 +392,22 @@ class Updater: ["git", "branch", "--set-upstream-to", f"origin/{branch}"], ["git", "reset", "--hard"], ["git", "clean", "-xdff"], - ["git", "submodule", "sync"], - ["git", "submodule", "update", "--init", "--recursive"], - ["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], ] + if force: + cmds += [ + ["git", "submodule", "sync", "--recursive"], + ["git", "submodule", "deinit", "--force", "--all"], + ["git", "clean", "-xdff"], + ["git", "submodule", "update", "--init", "--force", "--recursive"], + ["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], + ["git", "submodule", "foreach", "--recursive", "git", "clean", "-xdff"], + ] + else: + cmds += [ + ["git", "submodule", "sync"], + ["git", "submodule", "update", "--init", "--recursive"], + ["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], + ] r = [run(cmd, OVERLAY_MERGED) for cmd in cmds] cloudlog.info("git reset success: %s", '\n'.join(r)) @@ -463,13 +485,13 @@ def main() -> None: # download update last_fetch = params.get("UpdaterLastFetchTime") timed_out = last_fetch is None or (datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_fetch > datetime.timedelta(days=3)) - user_requested_fetch = wait_helper.user_request == UserRequest.FETCH + user_requested_fetch = wait_helper.user_request in (UserRequest.FETCH, UserRequest.FORCE_FETCH) if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch: cloudlog.info("skipping fetch, connection metered") elif wait_helper.user_request == UserRequest.CHECK: cloudlog.info("skipping fetch, only checking") else: - updater.fetch_update() + updater.fetch_update(force=(wait_helper.user_request == UserRequest.FORCE_FETCH)) write_time_to_param(params, "UpdaterLastFetchTime") update_failed_count = 0 except subprocess.CalledProcessError as e: