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) <noreply@anthropic.com>
This commit is contained in:
Amy Jeanes
2026-09-13 16:12:56 +01:00
committed by GitHub
parent b67898fac4
commit 885f447049
3 changed files with 51 additions and 17 deletions
@@ -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)
@@ -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)
@@ -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,