Compare commits

...

2 Commits

Author SHA1 Message Date
Amy Jeanes a5f44653d7 mici: add a refresh models button to the models panel (#2018)
models: add a refresh models button to mici, gate and show progress on both panels

Adds the refresh-models tile to the mici models panel (it was missing there),
factoring the sync-key trigger + in-progress check into refresh_model_list()
and refresh_in_progress() shared with the big UI.

On both UIs the refresh button is now gated on offroad + not-downloading +
not-refreshing (the manager runs offroad-only and its per-tick manifest fetch
sits above a blocking download loop, so a refresh queued in either state would
stick), and shows progress while the manager refetches: mici shows "fetching..."
on the tile, the big UI flips its button from REFRESH to "FETCHING..." to match
its FETCHING.../SELECT/CLEAR label style (as in the OSM panel), replacing the
old fire-and-forget popup.


Claude-Session: https://claude.ai/code/session_01EGMnVnSk5inGTrd7kuDVG9

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-14 08:33:48 -07:00
Amy Jeanes 63a2a3868e models: don't freeze the ui on an unset LagdToggleDelay (#2026)
models: don't block the ui on an unset LagdToggleDelay

Params.get's second positional is `block`, not a fallback value, so
get("LagdToggleDelay", "0.2") passes block=True and does a blocking read.
When the param is unset this spins the ui thread until it appears, freezing
the models panel (the description is rebuilt every frame). Read it the same
way livedelay/lagd_toggle.py does.


Claude-Session: https://claude.ai/code/session_01EGMnVnSk5inGTrd7kuDVG9

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-13 13:31:13 -07:00
3 changed files with 51 additions and 8 deletions
@@ -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"),
@@ -115,7 +116,7 @@ class ModelsLayout(Widget):
if lagd_toggle:
desc += f"<br>{tr('Live Steer Delay:')} {ui_state.sm['lateralDelay'].lateralDelay:.3f} s"
elif ui_state.CP is not None:
sw = float(ui_state.params.get("LagdToggleDelay", "0.2"))
sw = float(ui_state.params.get("LagdToggleDelay", return_default=True))
cp = ui_state.CP.steerActuatorDelay
desc += f"<br>{tr('Actuator Delay:')} {cp:.2f} s + {tr('Software Delay:')} {sw:.2f} s = {tr('Total Delay:')} {cp + sw:.2f} s"
self.lagd_toggle.set_description(desc)
@@ -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)
@@ -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)
@@ -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)