diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index a81679b296..d036d8d281 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -13,7 +13,7 @@ from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected from openpilot.common.constants import CV from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.sunnypilot.model_info import (big_model_state, bundles_for_source, carrying_model, default_model_name, - model_cache_size_mb, queued_name) + model_cache_size_mb, queued_name, refresh_in_progress, refresh_model_list) from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import DialogResult, Widget @@ -40,6 +40,8 @@ class ModelsLayout(Widget): self._downloading = False self._verifying = False self._clearing = False + self._refreshing = False + self._refresh_start: float | None = None self._last_note = None self.last_cache_calc_time = 0 @@ -67,10 +69,9 @@ class ModelsLayout(Widget): self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) - self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", - lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), - ui_state.params.put("ModelManager_LastSyncTime_Chestnut", 0), - gui_app.push_widget(alert_dialog(tr("Fetching Latest Models"))))) + self.refresh_item = button_item(tr("Refresh Model List"), + lambda: tr("FETCHING...") if self._refreshing else tr("REFRESH"), "", + self._refresh_models) self.clear_cache_item = ListItemSP( title=tr("Clear Model Cache"), @@ -133,6 +134,10 @@ class ModelsLayout(Widget): tr("Clear Cache"), callback=_callback) gui_app.push_widget(dialog) + def _refresh_models(self): + refresh_model_list() + self._refresh_start = time.monotonic() + def _handle_bundle_download_progress(self): self.cancel_download_item.set_visible(False) self._downloading = False @@ -343,6 +348,10 @@ class ModelsLayout(Widget): # manager is offroad-only, so an onroad clear would never be serviced self.clear_cache_item.action_item.set_enabled(offroad and not self._downloading and not self._clearing) + # manager is offroad-only, so a refresh queued onroad would never be serviced + self._refreshing = refresh_in_progress(self._refresh_start) + self.refresh_item.action_item.set_enabled(offroad and not self._downloading and not self._refreshing) + def _render(self, rect): self._scroller.render(rect) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py index 7522be9d64..404ff2e305 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -14,7 +14,8 @@ from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model, - default_model_name, model_cache_size_mb, model_info, queued_name) + default_model_name, model_cache_size_mb, model_info, queued_name, + refresh_in_progress, refresh_model_list) from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget @@ -83,6 +84,10 @@ class ModelsLayoutMici(NavScroller): self.select_model_btn = BigButton(tr("select model")) self.select_model_btn.set_click_callback(self._show_folders) + self.refresh_btn = BigButton(tr("refresh models")) + self.refresh_btn.set_click_callback(self._refresh_models) + self._refresh_start: float | None = None + self.cancel_download_btn = BigButton(tr("cancel download")) self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadRef")) @@ -90,7 +95,7 @@ class ModelsLayoutMici(NavScroller): self.clear_cache_btn.set_click_callback(self._confirm_clear_cache) self._cache_size_time = 0.0 - self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.clear_cache_btn] + self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn] self._scroller.add_widgets(self.main_items) @property @@ -173,6 +178,10 @@ class ModelsLayoutMici(NavScroller): gui_app.push_widget(BigConfirmationDialog(f"{tr('slide to')}\n{tr('clear cache')}", icon, lambda: ui_state.params.put_bool("ModelManager_ClearCache", True), red=True)) + def _refresh_models(self): + refresh_model_list() + self._refresh_start = time.monotonic() + def _select_folder(self, folder_name): source = self._selection_source if source is None: # folders are only reachable after picking a hardware @@ -226,6 +235,11 @@ class ModelsLayoutMici(NavScroller): self._cache_size_time = now self.clear_cache_btn.set_value(f"{model_cache_size_mb():.1f} MB") + # manager is offroad-only, so a refresh queued onroad would never be serviced + refreshing = refresh_in_progress(self._refresh_start) + self.refresh_btn.set_enabled(ui_state.is_offroad() and not is_downloading and not refreshing) + self.refresh_btn.set_value(tr("fetching...") if refreshing else "") + self.current_model_info.current_model_header.set_text(tr("active model")) active_text, info_header, info_text = _model_info() self.current_model_info.current_model_text.set_text(active_text) diff --git a/openpilot/selfdrive/ui/sunnypilot/model_info.py b/openpilot/selfdrive/ui/sunnypilot/model_info.py index a284acfca8..3820e4daa9 100644 --- a/openpilot/selfdrive/ui/sunnypilot/model_info.py +++ b/openpilot/selfdrive/ui/sunnypilot/model_info.py @@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ import contextlib import os +import time from openpilot.common.hardware.hw import Paths from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState @@ -98,3 +99,22 @@ def model_info() -> tuple[str, str, str]: active_name = active_bundle.displayName if active_bundle else default_model_name(source) other_name = other_bundle.displayName if other_bundle else default_model_name(other) return source, active_name, other_name + + +# mirrors the manager's ModelCache keys; the manager restamps them on a successful fetch +MODEL_SYNC_KEYS = ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut") +MODEL_SYNC_TIMEOUT = 20.0 + + +def refresh_model_list() -> None: + # zeroing the sync keys makes the manager refetch each manifest on its next tick + for key in MODEL_SYNC_KEYS: + ui_state.params.put(key, 0) + + +def refresh_in_progress(started_at: float | None) -> bool: + """Whether a user refresh is still outstanding. A failed fetch never restamps the + sync keys, so the spinner is bounded by MODEL_SYNC_TIMEOUT rather than sticking.""" + if started_at is None or time.monotonic() - started_at > MODEL_SYNC_TIMEOUT: + return False + return not all(ui_state.params.get(key) for key in MODEL_SYNC_KEYS)