Compare commits

...

3 Commits

Author SHA1 Message Date
nayan d68a6cd87d zoom zoom 2026-08-28 21:11:29 -04:00
Nayan 4075befc5e osm: support map deletion via sunnylink (#1971)
delete delete
2026-08-27 11:23:26 -04:00
Jason Wen 9f43d2477d [MICI] ui: move and restyle the sunnylink pill in settings (#1972) 2026-08-27 03:52:57 -04:00
7 changed files with 84 additions and 44 deletions
+1
View File
@@ -246,6 +246,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
{"Mapd_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"MapdVersion", {PERSISTENT, STRING}},
{"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}},
{"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}},
@@ -8,7 +8,6 @@ import datetime
import os
import platform
import requests
import shutil
import threading
from pathlib import Path
from time import monotonic
@@ -75,22 +74,12 @@ class OSMLayout(Widget):
def _update_map_size(self):
threading.Thread(target=self.calculate_size, daemon=True).start()
def _do_delete_maps(self):
if MAP_PATH.exists():
shutil.rmtree(MAP_PATH)
for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"):
ui_state.params.remove(param)
def _on_confirm_delete_maps(self):
ui_state.params.put_bool("Mapd_ClearCache", True)
self._delete_maps_btn.action_item.set_enabled(True)
self._delete_maps_btn.action_item.set_text(tr("DELETE"))
self._update_map_size()
def _on_confirm_delete_maps(self):
self._delete_maps_btn.action_item.set_enabled(False)
self._delete_maps_btn.action_item.set_text("DELETING...")
threading.Thread(target=self._do_delete_maps).start()
def _delete_maps(self):
self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
tr("Yes, delete all maps"), self._on_confirm_delete_maps)
@@ -12,13 +12,23 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, Bi
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
ICON_SIZE = 70
BIG_ICON_SIZE = 110
class SunnylinkBigButton(SettingsBigButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._label.set_font_weight(FontWeight.AUDIOWIDE)
def _get_label_font_size(self):
# Audiowide runs wider than Inter: "sunnylink" wraps to two lines at 64
return 56
class SettingsLayoutSP(OP.SettingsLayout):
def __init__(self):
OP.SettingsLayout.__init__(self)
@@ -33,7 +43,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
sunnylink_panel = SunnylinkLayoutMici()
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
sunnylink_btn = SunnylinkBigButton(tr("sunnylink"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/sunnylink.png", 76, 44))
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
models_panel = ModelsLayoutMici()
@@ -56,8 +66,8 @@ class SettingsLayoutSP(OP.SettingsLayout):
items = self._scroller._items.copy()
items.insert(1, sunnylink_btn)
items.insert(2, models_btn)
items.insert(1, models_btn)
items.insert(5, sunnylink_btn)
# front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad
items.insert(0, self._enable_offroad_btn_onroad)
+17
View File
@@ -55,6 +55,19 @@ def cleanup_old_osm_data(files_to_remove: list[str]) -> None:
shutil.rmtree(file, ignore_errors=False)
def clear_downloaded_maps() -> None:
"""Deletes downloaded OSM map data and resets params."""
path = f"{Paths.mapd_root()}/offline"
if os.path.exists(path):
shutil.rmtree(path, ignore_errors=True)
for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle",
"OsmStateName", "OsmStateTitle"):
params.remove(param)
cloudlog.info("mapd: downloaded maps cleared")
def request_refresh_osm_location_data(nations: list[str], states: list[str] | None = None) -> None:
params.put("OsmDownloadedDate", str(datetime.now().timestamp()), block=True)
params.put_bool("OsmDbUpdatesCheck", False, block=True)
@@ -131,6 +144,10 @@ def main_thread():
show_alert = bool(get_files_for_cleanup() and params.get_bool("OsmLocal"))
set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.")
if params.get("Mapd_ClearCache"):
clear_downloaded_maps()
params.remove("Mapd_ClearCache")
update_osm_db()
live_map_sp.tick()
rk.keep_time()
+2 -7
View File
@@ -41,12 +41,7 @@ def _compute_hash(file_path: str) -> str | None:
return None
async def verify_file(file_path: str, expected_hash: str) -> bool:
file_hash = _compute_hash(file_path)
return file_hash == expected_hash.lower() if file_hash else False
def _verify_file(file_path: str, expected_hash: str) -> bool:
def verify_file(file_path: str, expected_hash: str) -> bool:
file_hash = _compute_hash(file_path)
return file_hash == expected_hash.lower() if file_hash else False
@@ -81,7 +76,7 @@ def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[s
def _bundle_is_valid_locally(bundle: custom.ModelManagerSP.ModelBundle) -> bool:
model_root = Paths.model_root()
return all(_verify_file(os.path.join(model_root, file_name), expected_hash)
return all(verify_file(os.path.join(model_root, file_name), expected_hash)
for file_name, expected_hash in _bundle_artifacts(bundle))
+45 -20
View File
@@ -10,6 +10,7 @@ import os
import time
import requests
from openpilot.common.file_chunker import get_chunk_name
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
@@ -22,6 +23,8 @@ from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_
# (connect, read) seconds. read is per-request inactivity, not a total cap
DOWNLOAD_TIMEOUT = (30, 30)
# how many download+verify rounds before giving up on a chunk that won't verify
MAX_CHUNK_VERIFY_ATTEMPTS = 3
class DownloadCancelled(Exception):
@@ -153,6 +156,35 @@ class ModelManagerSP:
os.remove(base_path)
del self._download_start_times[artifact.fileName]
async def _verify_chunks_parallel(self, artifact, full_path: str) -> set[int]:
num_chunks = len(artifact.chunks)
start = time.monotonic()
futures = []
chunk_index: dict[asyncio.Future, int] = {}
for i, chunk in enumerate(artifact.chunks):
fut = asyncio.ensure_future(asyncio.to_thread(verify_file, get_chunk_name(full_path, i, num_chunks), chunk.sha256))
futures.append(fut)
chunk_index[fut] = i
valid_chunks: set[int] = set()
pending = set(futures)
while pending:
if self._download_interrupted():
for t in pending:
t.cancel()
raise DownloadCancelled("Download cancelled")
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
for fut in done:
if fut.result():
valid_chunks.add(chunk_index[fut])
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.verifying
artifact.downloadProgress.progress = (len(valid_chunks) / num_chunks) * 100
self._sync_artifact_progress(artifact)
self._report_status()
elapsed = time.monotonic() - start
cloudlog.info(f"Verified {len(valid_chunks)}/{num_chunks} chunks of {artifact.fileName} in {elapsed:.2f}s")
return valid_chunks
async def _process_artifact(self, artifact, destination_path: str) -> None:
if not artifact.downloadUri.uri:
return None
@@ -170,20 +202,10 @@ class ModelManagerSP:
is_cached = False
valid_chunks: set[int] = set()
if len(artifact.chunks) > 0:
from openpilot.common.file_chunker import get_chunk_name
num_chunks = len(artifact.chunks)
for i, chunk in enumerate(artifact.chunks):
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
if await verify_file(get_chunk_name(full_path, i, num_chunks), chunk.sha256):
valid_chunks.add(i)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.verifying
artifact.downloadProgress.progress = (len(valid_chunks) / num_chunks) * 100
self._sync_artifact_progress(artifact)
self._report_status()
is_cached = len(valid_chunks) == num_chunks
valid_chunks = await self._verify_chunks_parallel(artifact, full_path)
is_cached = len(valid_chunks) == len(artifact.chunks)
else:
if await verify_file(full_path, expected_hash):
if verify_file(full_path, expected_hash):
is_cached = True
if is_cached:
@@ -195,15 +217,18 @@ class ModelManagerSP:
return
if len(artifact.chunks) > 0:
await self._download_chunked(url, full_path, artifact, skip=valid_chunks)
from openpilot.common.file_chunker import get_chunk_name
for i, chunk in enumerate(artifact.chunks):
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
if not await verify_file(chunk_path, chunk.sha256):
raise ValueError(f"Hash validation failed for chunk {i+1} of {filename}")
attempts = 0
while len(valid_chunks) < len(artifact.chunks):
attempts += 1
cloudlog.warning(f"Re-downloading {len(artifact.chunks) - len(valid_chunks)} invalid chunk(s) of {filename} (attempt {attempts})")
await self._download_chunked(url, full_path, artifact, skip=valid_chunks)
valid_chunks = await self._verify_chunks_parallel(artifact, full_path)
if len(valid_chunks) < len(artifact.chunks) and attempts >= MAX_CHUNK_VERIFY_ATTEMPTS:
missing = next(i for i in range(len(artifact.chunks)) if i not in valid_chunks)
raise ValueError(f"Hash validation failed for chunk {missing+1} of {filename} after {attempts} attempts")
else:
await self._download_file(url, full_path, artifact)
if not await verify_file(full_path, expected_hash):
if not verify_file(full_path, expected_hash):
raise ValueError(f"Hash validation failed for {filename}")
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:447099e93e303b29e7b3eac237bb0f27f8c5e12786991139aee2432532a75f58
size 12310