mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-11 00:23:43 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d68a6cd87d | |||
| 4075befc5e | |||
| 9f43d2477d |
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user