diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index e155a68d4a..84fe6b3cc1 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -211,6 +211,8 @@ jobs: needs: [ prepare_strategy ] runs-on: ubuntu-24.04 if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }} + outputs: + onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }} env: HF_REPO: sunnypilot/sunnypilot_models_v1 HF_DEFAULTS_PATH: models/defaults/big @@ -226,6 +228,7 @@ jobs: run: | ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) echo "Repo ONNX hash: $ACTUAL_ONNX_HASH" + echo "onnx_sha256=$ACTUAL_ONNX_HASH" >> $GITHUB_OUTPUT JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" @@ -267,36 +270,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Download big model chunks - run: | - ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) - JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" - DEFAULTS=$(curl -fsSL "$JSON_URL") - BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') - - mkdir -p big_model_chunks - ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') - BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') - NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') - - CANONICAL="big_driving_tinygrad.pkl" - echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do - CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+') - CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" - ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") - echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" - curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL" - done - - echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" - - - name: Upload big model chunks - uses: actions/upload-artifact@v4 - with: - name: big-model-chunks - path: big_model_chunks/ - compression-level: 0 - - name: Cancel run on failure if: failure() run: gh run cancel ${{ github.run_id }} @@ -339,12 +312,32 @@ jobs: mkdir -p "${{ github.workspace }}/chestnut_output" tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output" - - name: Download big model chunks + - name: Download big model chunks from HF if: ${{ needs.prepare_chestnut.result == 'success' }} - uses: actions/download-artifact@v4 - with: - name: big-model-chunks - path: big_model_chunks + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/big + run: | + ONNX_HASH="${{ needs.prepare_chestnut.outputs.onnx_sha256 }}" + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + DEFAULTS=$(curl -fsSL "$JSON_URL") + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + + mkdir -p big_model_chunks + ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') + BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') + NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') + + CANONICAL="big_driving_tinygrad.pkl" + echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+') + CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" + curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL" + done + + echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" - name: Inject big model into chestnut if: ${{ needs.prepare_chestnut.result == 'success' }} diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index e083dfb079..becdceaaf0 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -178,27 +178,14 @@ class ModelsLayout(Widget): # circled_slash is authored grey; tinting it again only darkens it return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE} - @staticmethod - def _show_reset_params_dialog(): - def _callback(response): - if response == DialogResult.CONFIRM: - ui_state.params.remove("CalibrationParams") - ui_state.params.remove("LiveTorqueParameters") - msg = tr("Model download has started in the background. We suggest resetting calibration. Would you like to do that now?") - dialog = ConfirmDialog(msg, tr("Reset Calibration"), callback=_callback) - gui_app.push_widget(dialog) - def _on_model_selected(self, result): if result != DialogResult.CONFIRM: return selected_ref = self.model_dialog.selection_ref if selected_ref == "Default": ui_state.params.remove("ModelManager_ActiveBundle") - self._show_reset_params_dialog() elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None): ui_state.params.put("ModelManager_DownloadIndex", selected_bundle.index) - if self.model_manager.activeBundle and selected_bundle.generation != self.model_manager.activeBundle.generation: - self._show_reset_params_dialog() self.model_dialog = None @staticmethod diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py index 3cf639d0a1..4be82bef4a 100644 --- a/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py @@ -8,6 +8,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath +from openpilot.selfdrive.ui.sunnypilot.ui_state import MADSState from openpilot.system.ui.lib.application import gui_app @@ -19,6 +20,11 @@ class ModelRendererSP: @property def _lateral_active(self) -> bool: + sm = ui_state.sm + if sm.valid["selfdriveStateSP"]: + mads = sm["selfdriveStateSP"].mads + if mads.available: + return mads.enabled and mads.state != MADSState.paused return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) def _get_path_half_width(self) -> float: diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 7ba805ff95..8ca8775875 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -143,13 +143,17 @@ class ModelManagerSP: is_cached = False if len(artifact.chunks) > 0: from openpilot.common.file_chunker import get_chunk_name + num_chunks = len(artifact.chunks) chunks_valid = True for i, chunk in enumerate(artifact.chunks): - chunk_path = get_chunk_name(full_path, i, len(artifact.chunks)) + chunk_path = get_chunk_name(full_path, i, num_chunks) if not await verify_file(chunk_path, chunk.sha256): chunks_valid = False break - if chunks_valid and len(artifact.chunks) > 0: + artifact.downloadProgress.progress = ((i + 1) / num_chunks) * 100 + self._sync_artifact_progress(artifact) + self._report_status() + if chunks_valid and num_chunks > 0: is_cached = True else: if await verify_file(full_path, expected_hash): @@ -216,6 +220,9 @@ class ModelManagerSP: """Downloads all models in a bundle""" self.selected_bundle = model_bundle self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloading + for model in self.selected_bundle.models: + model.artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading + self._report_status() os.makedirs(destination_path, exist_ok=True) try: @@ -260,7 +267,9 @@ class ModelManagerSP: self.active_bundle = get_active_bundle(self.params) if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: - if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): + if self.active_bundle and self.active_bundle.index == index_to_download: + self.params.remove("ModelManager_DownloadIndex") + elif model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): try: self.download(model_to_download, Paths.model_root()) except Exception as e: diff --git a/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py index ac6ab6aaea..2e99122bc7 100644 --- a/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py +++ b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py @@ -83,9 +83,14 @@ class TestLocationdProc(OpenpilotTestCase): self.pm.send(msg.which(), msg) if msg.which() == "cameraOdometry": self.pm.wait_for_readers_to_update(msg.which(), timeout=1, dt=0.005) - time.sleep(1) # wait for async params write + for _ in range(50): + val = self.params.get('LastGPSPositionLLK') + if val is not None: + break + time.sleep(0.1) - lastGPS = json.loads(self.params.get('LastGPSPositionLLK')) + self.assertIsNotNone(val, "LastGPSPositionLLK not written within 5s") + lastGPS = json.loads(val) self.assertAlmostEqual(lastGPS['latitude'], self.lat, delta=0.001) self.assertAlmostEqual(lastGPS['longitude'], self.lon, delta=0.001) self.assertAlmostEqual(lastGPS['altitude'], self.alt, delta=0.001) diff --git a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index c0534a76bf..27be6e24ed 100755 --- a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -28,7 +28,8 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import openpilot.cereal.messaging as messaging -from openpilot.sunnypilot.models.default_model import get_default_model +from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled +from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string @@ -181,7 +182,10 @@ def getParamsMetadata() -> str: schema = generate_schema() schema["capabilities"] = generate_capabilities() schema["capability_labels"] = CAPABILITY_LABELS - schema["default_model"] = get_default_model() + # mirrors get_default_model() — ui_state unavailable in sunnylinkd process + show_big = (usbgpu_present() and usbgpu_compiled() + and (params.get_bool("UsbGpuActive") or params.get_bool("UsbGpuLoading") or params.get_bool("IsOffroad"))) + schema["default_model"] = DEFAULT_BIG_MODEL if show_big else DEFAULT_MODEL raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8") except Exception: