From 7430f245c7e2d3a67ae10f3b05ffdc44baacc358 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Sun, 13 Sep 2026 16:12:56 +0100 Subject: [PATCH] mici: add a clear cache button to the models panel (#2008) models: add a clear cache button to mici, gate and show progress on both panels Adds the clear-cache tile to the mici models panel (trash slide-to-confirm), factoring the cache-size math into model_cache_size_mb() shared with the big UI. On both UIs the clear button is now gated on offroad + not-downloading + not-clearing (the manager runs offroad-only, so a clear queued onroad would never be serviced and would stick), and shows progress while the manager works: mici shows "clearing..." on the tile, the big UI flips its button to "CLEARING..." to match its FETCHING.../SELECT/CLEAR label style (as in the OSM panel). Claude-Session: https://claude.ai/code/session_01EGMnVnSk5inGTrd7kuDVG9 Co-authored-by: Claude Opus 4.8 (1M context) --- .../ui/sunnypilot/layouts/settings/models.py | 26 +++++++++--------- .../ui/sunnypilot/mici/layouts/models.py | 27 ++++++++++++++++--- .../selfdrive/ui/sunnypilot/model_info.py | 15 +++++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index d1abfa22a0..86d3e1e513 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -4,7 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import os import re import time import pyray as rl @@ -13,7 +12,8 @@ from openpilot.cereal import custom from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle, resolve_bundle_by_ref 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, queued_name +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) 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 @@ -21,7 +21,6 @@ from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDial from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets.toggle import ON_COLOR -from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction, ScrollingButtonAction from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp @@ -40,6 +39,7 @@ class ModelsLayout(Widget): self._selection_source = None self._downloading = False self._verifying = False + self._clearing = False self._last_note = None self.last_cache_calc_time = 0 @@ -75,7 +75,7 @@ class ModelsLayout(Widget): self.clear_cache_item = ListItemSP( title=tr("Clear Model Cache"), description="", - action_item=NoElideButtonAction(tr("CLEAR")), + action_item=NoElideButtonAction(lambda: tr("CLEARING...") if self._clearing else tr("CLEAR")), callback=self._clear_cache ) @@ -122,20 +122,12 @@ class ModelsLayout(Widget): @staticmethod def calculate_cache_size(): - cache_size = 0.0 - if os.path.exists(CUSTOM_MODEL_PATH): - for file in os.listdir(CUSTOM_MODEL_PATH): - try: - cache_size += os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) - except OSError: - continue - return cache_size / (1024**2) + return model_cache_size_mb() def _clear_cache(self): def _callback(response): if response == DialogResult.CONFIRM: ui_state.params.put_bool("ModelManager_ClearCache", True) - self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") dialog = ConfirmDialog(tr("This will delete ALL downloaded models from the cache except the currently active model. Are you sure?"), tr("Clear Cache"), callback=_callback) @@ -147,7 +139,10 @@ class ModelsLayout(Widget): self._verifying = False self.download_item.set_visible(True) - if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5: + self._clearing = ui_state.params.get_bool("ModelManager_ClearCache") + if self._clearing: + self.last_cache_calc_time = 0.0 # refresh the size as soon as clearing finishes + elif (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5: self.last_cache_calc_time = current_time self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") @@ -345,6 +340,9 @@ class ModelsLayout(Widget): self.big_model_item.action_item.set_enabled(offroad) self.small_model_item.set_description("" if offroad else tr("Only available when vehicle is off, or always offroad mode is on")) + # 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) + 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 4c769498f5..7522be9d64 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -4,15 +4,17 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import time + import pyray as rl from openpilot.cereal import custom -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog +from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle 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_info, queued_name) + default_model_name, model_cache_size_mb, model_info, queued_name) 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 @@ -84,7 +86,11 @@ class ModelsLayoutMici(NavScroller): self.cancel_download_btn = BigButton(tr("cancel download")) self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadRef")) - self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn] + self.clear_cache_btn = BigButton(tr("clear cache"), value=f"{model_cache_size_mb():.1f} MB") + 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._scroller.add_widgets(self.main_items) @property @@ -162,6 +168,11 @@ class ModelsLayoutMici(NavScroller): ui_state.params.remove(ACTIVE_BUNDLE_KEYS[source]) self._pop_to_main() + def _confirm_clear_cache(self): + icon = gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64) + 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 _select_folder(self, folder_name): source = self._selection_source if source is None: # folders are only reachable after picking a hardware @@ -205,6 +216,16 @@ class ModelsLayoutMici(NavScroller): device.set_override_interactive_timeout(None) self._was_downloading = is_downloading + # manager is offroad-only, so an onroad clear would never be serviced + clearing = ui_state.params.get_bool("ModelManager_ClearCache") + self.clear_cache_btn.set_enabled(ui_state.is_offroad() and not is_downloading and not clearing) + if clearing: + self.clear_cache_btn.set_value(tr("clearing...")) + self._cache_size_time = 0.0 # refresh the size as soon as clearing finishes + elif (now := time.monotonic()) - self._cache_size_time > 0.5: + self._cache_size_time = now + self.clear_cache_btn.set_value(f"{model_cache_size_mb():.1f} MB") + 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 e338feb9ed..a284acfca8 100644 --- a/openpilot/selfdrive/ui/sunnypilot/model_info.py +++ b/openpilot/selfdrive/ui/sunnypilot/model_info.py @@ -4,12 +4,27 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import contextlib +import os + +from openpilot.common.hardware.hw import Paths from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.sunnypilot.models.fetcher import get_cached_bundles from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL +def model_cache_size_mb() -> float: + """Bytes on disk under the model cache directory, in MB.""" + model_root = Paths.model_root() + total = 0 + if os.path.isdir(model_root): + for name in os.listdir(model_root): + with contextlib.suppress(OSError): + total += os.path.getsize(os.path.join(model_root, name)) + return total / (1024 ** 2) + + def active_source() -> str: return get_active_source(chestnut=ui_state.chestnut_present, chestnut_active=ui_state.chestnut_active, chestnut_loading=ui_state.chestnut_loading,