From 78051cc14b5d1aff702d2a3ff41a4de74f72a7ee Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 28 Aug 2026 12:58:26 -0400 Subject: [PATCH] =?UTF-8?q?Sync:=20`commaai/openpilot:master`=20=E2=86=92?= =?UTF-8?q?=20`sunnypilot/sunnypilot:master`=20(#1973)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto_pr_review.yaml | 91 +++ .../workflows/build-all-tinygrad-models.yaml | 166 +++++ .github/workflows/build-default-models.yaml | 501 +++++++++++++++ .../build-single-tinygrad-model.yaml | 236 +++++++ .github/workflows/cereal_validation.yaml | 97 +++ .github/workflows/diff_report.yaml | 58 ++ .github/workflows/docs.yaml | 69 ++ .../download-hf-model-chunks/action.yml | 66 ++ .github/workflows/jenkins-pr-trigger.yaml | 96 +++ .github/workflows/lfs-maintenance.yaml | 85 +++ .../workflows/post-to-discourse/action.yml | 105 +++ .github/workflows/release-drafter.yml | 28 + .github/workflows/release.yaml | 36 ++ .github/workflows/repo-maintenance.yaml | 132 ++++ .github/workflows/stale.yaml | 52 ++ .github/workflows/sunnypilot-build-model.yaml | 301 +++++++++ .../workflows/sunnypilot-build-prebuilt.yaml | 604 ++++++++++++++++++ .../workflows/sunnypilot-master-dev-prep.yaml | 243 +++++++ .github/workflows/test-discourse.yaml.yml | 78 +++ .github/workflows/tests.yaml | 243 +++++++ .github/workflows/ui_preview.yaml | 176 +++++ .github/workflows/wait-for-action/action.yaml | 52 ++ openpilot/common/params_keys.h | 11 +- .../icons_mici/{egpu.png => chestnut.png} | Bin .../{egpu_green.png => chestnut_green.png} | Bin .../assets/icons_mici/chestnut_orange.png | 3 + .../assets/icons_mici/egpu_crossed.png | Bin 2034 -> 0 bytes .../selfdrive/assets/icons_mici/egpu_gray.png | Bin 7279 -> 0 bytes .../assets/icons_mici/egpu_orange.png | Bin 10876 -> 0 bytes openpilot/selfdrive/modeld/SConscript | 92 +-- .../selfdrive/modeld/dmonitoringmodeld.py | 2 +- openpilot/selfdrive/modeld/helpers.py | 14 +- openpilot/selfdrive/modeld/modeld.py | 62 +- openpilot/selfdrive/selfdrived/selfdrived.py | 8 +- openpilot/selfdrive/ui/mici/layouts/home.py | 16 +- .../selfdrive/ui/mici/onroad/hud_renderer.py | 61 +- .../ui/sunnypilot/layouts/settings/models.py | 20 +- .../ui/sunnypilot/layouts/sidebar.py | 32 +- .../ui/sunnypilot/mici/layouts/home.py | 45 +- .../ui/sunnypilot/mici/layouts/models.py | 8 +- .../ui/sunnypilot/mici/onroad/hud_renderer.py | 3 - .../selfdrive/ui/sunnypilot/model_info.py | 37 +- openpilot/selfdrive/ui/sunnypilot/ui_state.py | 7 +- openpilot/selfdrive/ui/ui_state.py | 63 +- .../sunnypilot/modeld_v2/compile_modeld.py | 2 +- openpilot/sunnypilot/modeld_v2/modeld.py | 42 +- .../sunnypilot/modeld_v2/tests/helpers.py | 4 +- .../tests/test_combined_pkl_loader.py | 4 +- openpilot/sunnypilot/models/default_model.py | 4 +- openpilot/sunnypilot/models/fetcher.py | 14 +- openpilot/sunnypilot/models/helpers.py | 22 +- openpilot/sunnypilot/models/manager.py | 6 +- .../models/tests/test_manager_download.py | 122 ++-- .../sunnypilot/sunnylink/athena/sunnylinkd.py | 2 +- openpilot/sunnypilot/sunnylink/statsd.py | 2 +- .../sunnypilot/system/params_migration.py | 12 +- .../system/tests/test_params_migration.py | 12 +- openpilot/system/hardware/hardwared.py | 4 +- 58 files changed, 3880 insertions(+), 371 deletions(-) create mode 100644 .github/workflows/auto_pr_review.yaml create mode 100644 .github/workflows/build-all-tinygrad-models.yaml create mode 100644 .github/workflows/build-default-models.yaml create mode 100644 .github/workflows/build-single-tinygrad-model.yaml create mode 100644 .github/workflows/cereal_validation.yaml create mode 100644 .github/workflows/diff_report.yaml create mode 100644 .github/workflows/docs.yaml create mode 100644 .github/workflows/download-hf-model-chunks/action.yml create mode 100644 .github/workflows/jenkins-pr-trigger.yaml create mode 100644 .github/workflows/lfs-maintenance.yaml create mode 100644 .github/workflows/post-to-discourse/action.yml create mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/repo-maintenance.yaml create mode 100644 .github/workflows/stale.yaml create mode 100644 .github/workflows/sunnypilot-build-model.yaml create mode 100644 .github/workflows/sunnypilot-build-prebuilt.yaml create mode 100644 .github/workflows/sunnypilot-master-dev-prep.yaml create mode 100644 .github/workflows/test-discourse.yaml.yml create mode 100644 .github/workflows/tests.yaml create mode 100644 .github/workflows/ui_preview.yaml create mode 100644 .github/workflows/wait-for-action/action.yaml rename openpilot/selfdrive/assets/icons_mici/{egpu.png => chestnut.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_green.png => chestnut_green.png} (100%) create mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_orange.png delete mode 100644 openpilot/selfdrive/assets/icons_mici/egpu_crossed.png delete mode 100644 openpilot/selfdrive/assets/icons_mici/egpu_gray.png delete mode 100644 openpilot/selfdrive/assets/icons_mici/egpu_orange.png diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml new file mode 100644 index 000000000..29149d865 --- /dev/null +++ b/.github/workflows/auto_pr_review.yaml @@ -0,0 +1,91 @@ +name: "PR review" +on: + pull_request_target: + types: [ opened, reopened, synchronize, edited ] + +jobs: + labeler: + name: review + permissions: + contents: read + pull-requests: write + issues: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + submodules: false + + # Label PRs + - uses: actions/labeler@v6 + with: + dot: true + configuration-path: .github/labeler.yaml + + # Check PR target branch + - name: check branch + uses: Vankka/pr-target-branch-action@5da68a42bcb7b43d39104295a876a6f3f8d7908b + if: github.repository == 'sunnypilot/sunnypilot' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + target: /^(?!master$).*/ + exclude: /sunnypilot:.*/ + change-to: ${{ github.base_ref }} + already-exists-action: close_this + already-exists-comment: "Your PR should be made against the `master` branch" + + update-pr-labels: + name: Update fork's PR Labels + runs-on: ubuntu-latest + if: (github.event.pull_request.head.repo.fork && (contains(github.event_name, 'pull_request') && github.event.action == 'synchronize')) + env: + PR_LABEL: 'dev' + TRUST_FORK_PR_LABEL: 'trust-fork-pr' + steps: + - name: Check if PR has dev label + id: check-labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const hasDevC3Label = labels.some(label => label.name === process.env.PR_LABEL); + const hasTrustLabel = labels.some(label => label.name === process.env.TRUST_FORK_PR_LABEL); + + console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); + console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); + + core.setOutput('has-dev', hasDevC3Label ? 'true' : 'false'); + core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); + + - name: Remove trust-fork-pr label if present + if: steps.check-labels.outputs.has-dev == 'true' && steps.check-labels.outputs.has-trust == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: process.env.TRUST_FORK_PR_LABEL + }); + + console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); + + // Add a comment to the PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `The \`${process.env.TRUST_FORK_PR_LABEL}\` label has been automatically removed because new commits were pushed to this PR. This PR will need to be re-reviewed before the label can be applied again.` + }); diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml new file mode 100644 index 000000000..7bddeab2b --- /dev/null +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -0,0 +1,166 @@ +name: Build and push all tinygrad models + +on: + workflow_dispatch: + inputs: + set_min_version: + description: 'Minimum selector version required for the models (see helpers.py or readme.md)' + required: true + type: string + target_hardware: + description: 'Hardware target to compile for (qcom or chestnut)' + required: true + type: choice + default: 'qcom' + options: + - qcom + - chestnut + hf_repo: + description: 'Hugging Face dataset repository' + required: false + type: string + default: 'sunnypilot/sunnypilot_models_v1' + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + json_version: ${{ steps.get-json.outputs.json_version }} + recompiled_dir: ${{ steps.create-recompiled-dir.outputs.recompiled_dir }} + json_file: ${{ steps.get-json.outputs.json_file }} + model_matrix: ${{ steps.set-matrix.outputs.model_matrix }} + tinygrad_ref: ${{ steps.get-tinygrad-ref.outputs.tinygrad_ref }} + steps: + - name: Checkout sunnypilot repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot + path: sunnypilot + submodules: recursive + + - name: Get tinygrad_repo ref + id: get-tinygrad-ref + run: | + cd sunnypilot + export PYTHONPATH=$(pwd) + ref=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py) + echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT + echo "tinygrad_ref is $ref" + + - name: Checkout docs repo (sunnypilot-models, gh-pages) + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-models + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Get next JSON version to use (from GitHub docs repo) + id: get-json + run: | + cd docs/docs + PREFIX="driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_' || '' }}v" + latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1) + next=$((latest+1)) + json_file="${PREFIX}${next}.json" + cp "${PREFIX}${latest}.json" "$json_file" + echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT + echo "json_version=$((next+0))" >> $GITHUB_OUTPUT + echo "SRC_JSON_FILE=docs/docs/${PREFIX}${latest}.json" >> $GITHUB_ENV + + - name: Extract tinygrad models + id: set-matrix + working-directory: docs/docs + run: | + jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json + echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + - name: Get next recompiled dir number + id: create-recompiled-dir + env: + HF_REPO: ${{ github.event.inputs.hf_repo }} + run: | + pip install huggingface_hub + recompiled_dir=$(python3 -c " + from huggingface_hub import HfApi + import re, sys + api = HfApi() + files = api.list_repo_files(repo_id=sys.argv[1], repo_type='dataset') + dirs = [re.search(r'models/recompiled([0-9]+)', f) for f in files] + nums = [int(m.group(1)) for m in dirs if m] + print(max(nums) + 1) + " "$HF_REPO") + echo "recompiled_dir=$recompiled_dir" >> $GITHUB_OUTPUT + + - name: Push new JSON to GitHub docs repo + run: | + cd docs + git pull origin gh-pages + git add docs/"$(basename ${{ steps.get-json.outputs.json_file }})" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Add new ${{ steps.get-json.outputs.json_file }} for build-all" || echo "No changes to commit" + git push origin gh-pages + + get_and_build: + needs: [setup] + strategy: + matrix: + model: ${{ fromJson(needs.setup.outputs.model_matrix) }} + fail-fast: false + uses: ./.github/workflows/build-single-tinygrad-model.yaml + with: + upstream_branch: ${{ matrix.model.ref }} + custom_name: ${{ matrix.model.display_name }} + is_20hz: ${{ matrix.model.is_20hz }} + recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} + json_version: ${{ needs.setup.outputs.json_version }} + target_hardware: ${{ github.event.inputs.target_hardware }} + hf_repo: ${{ github.event.inputs.hf_repo }} + set_min_version: ${{ github.event.inputs.set_min_version }} + tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }} + secrets: inherit + + retry_failed_models: + needs: [setup, get_and_build] + runs-on: ubuntu-latest + if: ${{ !cancelled() && needs.setup.result == 'success' && (needs.get_and_build.result == 'success' || needs.get_and_build.result == 'failure') }} + outputs: + retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }} + steps: + - uses: actions/download-artifact@v4 + with: + pattern: artifact-name-* + path: output + continue-on-error: true + + - id: set-retry-matrix + run: | + echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json + built=(); while IFS= read -r line; do [ -n "$line" ] && built+=("$line"); done < <( + find output -maxdepth 1 -name 'artifact-name-*' -printf "%f\n" 2>/dev/null | sed -E 's/^artifact-name-//' | awk '{gsub(/^ +| +$/, ""); print}' + ) + jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ + 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json + echo "retry_matrix=$(cat retry_matrix.json)" >> $GITHUB_OUTPUT + + retry_get_and_build: + needs: [setup, get_and_build, retry_failed_models] + if: ${{ !cancelled() && needs.retry_failed_models.result == 'success' && needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '' }} + strategy: + matrix: + model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }} + fail-fast: false + uses: ./.github/workflows/build-single-tinygrad-model.yaml + with: + upstream_branch: ${{ matrix.model.ref }} + custom_name: ${{ matrix.model.display_name }} + is_20hz: ${{ matrix.model.is_20hz }} + recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} + json_version: ${{ needs.setup.outputs.json_version }} + target_hardware: ${{ github.event.inputs.target_hardware }} + artifact_suffix: -retry + hf_repo: ${{ github.event.inputs.hf_repo }} + set_min_version: ${{ github.event.inputs.set_min_version }} + tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }} + secrets: inherit diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml new file mode 100644 index 000000000..8030c7133 --- /dev/null +++ b/.github/workflows/build-default-models.yaml @@ -0,0 +1,501 @@ +name: Build default models + +on: + workflow_dispatch: + inputs: + target: + description: 'Model target to build' + required: true + type: choice + options: + - small + - big + - dm + workflow_call: + inputs: + target: + description: 'Model target to build (small, big, or dm)' + required: true + type: string + +concurrency: + group: build-default-models-${{ inputs.target }} + cancel-in-progress: false + +env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + +jobs: + resolve: + runs-on: ubuntu-24.04 + outputs: + model_name: ${{ steps.resolve.outputs.model_name }} + onnx_ref: ${{ steps.resolve.outputs.onnx_ref }} + onnx_path: ${{ steps.resolve.outputs.onnx_path }} + hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }} + tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - id: resolve + run: | + export PYTHONPATH=${{ github.workspace }} + + if [ "${{ inputs.target }}" = "big" ]; then + NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)") + ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" + HF_DEFAULTS_PATH="models/defaults/big" + elif [ "${{ inputs.target }}" = "dm" ]; then + ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" + HF_DEFAULTS_PATH="models/defaults/dm" + NAME="dmonitoring_model ($(git log -1 --format=%cd --date=format:'%B %d, %Y' -- "$ONNX_PATH"))" + else + NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL; print(DEFAULT_MODEL)") + ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx" + HF_DEFAULTS_PATH="models/defaults/small" + fi + + ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH") + TINYGRAD_REF=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py) + if [ -z "$TINYGRAD_REF" ]; then + echo "::error::Failed to resolve tinygrad ref" + exit 1 + fi + + echo "model_name=${NAME}" >> $GITHUB_OUTPUT + echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT + echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT + echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT + echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT + + build_small_model: + needs: resolve + if: ${{ inputs.target == 'small' }} + runs-on: [self-hosted, tici] + env: + SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx + SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Pull ONNX via LFS + run: git lfs pull -I "${{ env.SMALL_ONNX }}" + + - name: Set environment variables + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + + - name: Disable powersave + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Compile small model with stock compiler + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") + CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") + FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)") + + TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + + env ${TG_FLAGS} python3 \ + ${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \ + --onnx ${{ github.workspace }}/${{ env.SMALL_ONNX }} \ + --model-size $MODEL_SIZE \ + --camera-resolutions $CAMERA_RES \ + --frame-skip $FRAME_SKIP \ + --output ${{ github.workspace }}/${{ env.SMALL_PKL }} + + - name: Chunk small pkl + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + python3 -c " + from openpilot.common.file_chunker import chunk_file, get_chunk_targets + import os + pkl = '${{ github.workspace }}/${{ env.SMALL_PKL }}' + size = os.path.getsize(pkl) + targets = get_chunk_targets(pkl, size) + chunk_file(pkl, targets) + print(f'Chunked into {len(targets)} files') + " + + - name: Prepare output + env: + MODEL_NAME: ${{ needs.resolve.outputs.model_name }} + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models" + OUTPUT_DIR="${{ github.workspace }}/small_output" + PKL_BASE="driving_tinygrad.pkl" + mkdir -p "$OUTPUT_DIR" + + cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/" + cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/" + + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --model-dir "$MODELS_DIR" \ + --output-dir "$OUTPUT_DIR" \ + --custom-name "$MODEL_NAME" \ + --upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" + + echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt" + + - name: Upload small model artifact + uses: actions/upload-artifact@v4 + with: + name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} + path: ${{ github.workspace }}/small_output/ + + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ needs.resolve.outputs.model_name }} + path: ${{ github.workspace }}/small_output/artifact_name.txt + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + + build_big_model: + needs: resolve + if: ${{ inputs.target == 'big' }} + runs-on: [self-hosted, chestnut] + env: + BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Pull big ONNX via LFS + run: git lfs pull -I "${{ env.BIG_ONNX }}" + + - name: Set environment variables + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + + - name: Disable powersave + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Wait for chestnut PCIe link + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + python3 -c " + import time + from openpilot.system.hardware.chestnut.flash import link_up + for i in range(10): + if link_up(): + print(f'PCIe link up after {i+1} attempt(s)') + break + time.sleep(1) + else: + raise RuntimeError('Chestnut PCIe link not ready after 10 attempts') + " + + - name: Compile big model with stock compiler + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") + CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") + FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)") + + TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" + + env ${TG_FLAGS} python3 \ + ${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \ + --onnx ${{ github.workspace }}/${{ env.BIG_ONNX }} \ + --model-size $MODEL_SIZE \ + --camera-resolutions $CAMERA_RES \ + --frame-skip $FRAME_SKIP \ + --output ${{ github.workspace }}/${{ env.BIG_PKL }} + + - name: Chunk big pkl + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + python3 -c " + from openpilot.common.file_chunker import chunk_file, get_chunk_targets + import os + pkl = '${{ github.workspace }}/${{ env.BIG_PKL }}' + size = os.path.getsize(pkl) + targets = get_chunk_targets(pkl, size) + chunk_file(pkl, targets) + print(f'Chunked into {len(targets)} files') + " + + - name: Prepare output + env: + MODEL_NAME: ${{ needs.resolve.outputs.model_name }} + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models" + OUTPUT_DIR="${{ github.workspace }}/big_output" + PKL_BASE="big_driving_tinygrad.pkl" + mkdir -p "$OUTPUT_DIR" + + cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/" + cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/" + + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --model-dir "$MODELS_DIR" \ + --output-dir "$OUTPUT_DIR" \ + --custom-name "$MODEL_NAME" \ + --upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" + + echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt" + + - name: Upload big model artifact + uses: actions/upload-artifact@v4 + with: + name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} + path: ${{ github.workspace }}/big_output/ + + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ needs.resolve.outputs.model_name }} + path: ${{ github.workspace }}/big_output/artifact_name.txt + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + + upload_defaults: + needs: [ resolve, build_small_model, build_big_model, build_dm_model ] + if: | + ${{ + !cancelled() && + (inputs.target == 'big' && needs.build_big_model.result == 'success' || + inputs.target == 'small' && needs.build_small_model.result == 'success' || + inputs.target == 'dm' && needs.build_dm_model.result == 'success') + }} + runs-on: ubuntu-24.04 + permissions: + id-token: write + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Pull ONNX via LFS + run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}" + + - name: Install huggingface_hub + run: pip install --upgrade "huggingface_hub>=0.22.0" + + - name: Download artifact name + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ needs.resolve.outputs.model_name }} + path: artifact_name + + - name: Read artifact name + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} + id: artifact + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download model artifact + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.artifact_name }} + path: output + + - name: Upload model to HF + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} + env: + HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} + run: | + rm -f output/artifact_name.txt + export PYTHONPATH=$(pwd) + python3 release/ci/upload_default_model.py \ + --hf-repo "${{ env.HF_REPO }}" \ + --hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \ + --artifact-name "$ARTIFACT_NAME" \ + --model-dir output \ + --onnx-path "${{ needs.resolve.outputs.onnx_path }}" \ + --onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \ + --model-name "${{ needs.resolve.outputs.model_name }}" \ + --tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \ + --run-number "${{ github.run_number }}" + + - name: Download DM artifact + if: ${{ inputs.target == 'dm' }} + uses: actions/download-artifact@v4 + with: + name: dm-model-${{ github.run_number }} + path: dm_output + + - name: Generate DM metadata and upload to HF + if: ${{ inputs.target == 'dm' }} + env: + HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + run: | + export PYTHONPATH=$(pwd) + python3 -c " + import json, hashlib + from pathlib import Path + from datetime import datetime, UTC + + dm_dir = Path('dm_output') + manifest = list(dm_dir.glob('*.chunkmanifest')) + assert manifest, 'No chunkmanifest found' + pkl_name = manifest[0].name.removesuffix('.chunkmanifest') + num_chunks = int(manifest[0].read_text().strip()) + + chunks = [] + for i in range(num_chunks): + chunk = dm_dir / f'{pkl_name}.chunk{i+1:02d}of{num_chunks:02d}' + chunks.append({ + 'file_name': chunk.name, + 'sha256': hashlib.sha256(chunk.read_bytes()).hexdigest() + }) + + digest = hashlib.sha256() + for c in chunks: + with open(dm_dir / c['file_name'], 'rb') as f: + while block := f.read(1024*1024): + digest.update(block) + + metadata = { + 'bundles': [{ + 'short_name': 'DMMODEL', + 'display_name': '${{ needs.resolve.outputs.model_name }}', + 'ref': '${{ needs.resolve.outputs.onnx_ref }}', + 'runner': 'tinygrad', + 'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'models': [{ + 'type': 'chunked', + 'artifact': { + 'file_name': pkl_name, + 'download_uri': {'url': '', 'sha256': digest.hexdigest()}, + 'chunks': chunks + } + }] + }] + } + with open(dm_dir / 'metadata.json', 'w') as f: + json.dump(metadata, f, indent=2) + print('Generated DM metadata.json') + " + + python3 release/ci/upload_default_model.py \ + --hf-repo "${{ env.HF_REPO }}" \ + --hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \ + --artifact-name "dm-model-${{ github.run_number }}" \ + --model-dir dm_output \ + --onnx-path "${{ needs.resolve.outputs.onnx_path }}" \ + --onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \ + --model-name "${{ needs.resolve.outputs.model_name }}" \ + --tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \ + --run-number "${{ github.run_number }}" + + build_dm_model: + needs: resolve + if: ${{ inputs.target == 'dm' }} + runs-on: [self-hosted, tici] + env: + DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx + DM_PKL: openpilot/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Pull DM ONNX via LFS + run: git lfs pull -I "${{ env.DM_ONNX }}" + + - name: Set environment variables + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + + - name: Disable powersave + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Compile DM model + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + + taskset -c 7 env ${TG_FLAGS} python3 \ + ${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile3.py \ + ${{ github.workspace }}/${{ env.DM_ONNX }} \ + ${{ github.workspace }}/${{ env.DM_PKL }} + + - name: Chunk DM pkl + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + python3 -c " + from openpilot.common.file_chunker import chunk_file, get_chunk_targets + import os + pkl = '${{ github.workspace }}/${{ env.DM_PKL }}' + size = os.path.getsize(pkl) + targets = get_chunk_targets(pkl, size) + chunk_file(pkl, targets) + print(f'Chunked {pkl} into {len(targets)} chunks') + " + + - name: Prepare DM output + run: | + mkdir -p dm_output + cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/ + cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/ + + - name: Upload DM artifact + uses: actions/upload-artifact@v4 + with: + name: dm-model-${{ github.run_number }} + path: dm_output/ + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + diff --git a/.github/workflows/build-single-tinygrad-model.yaml b/.github/workflows/build-single-tinygrad-model.yaml new file mode 100644 index 000000000..c9d495c15 --- /dev/null +++ b/.github/workflows/build-single-tinygrad-model.yaml @@ -0,0 +1,236 @@ +name: Build Single Tinygrad Model and Push + +on: + workflow_call: + inputs: + upstream_branch: + description: 'Upstream commit to build from' + required: true + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + recompiled_dir: + description: 'Existing recompiled directory number (e.g. 1 for recompiled1)' + required: true + type: string + json_version: + description: 'driving_models version number to update (e.g. 18 for driving_models_v18.json)' + required: true + type: string + artifact_suffix: + description: 'Suffix for artifact name' + required: false + type: string + default: '' + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: true + target_hardware: + description: 'Hardware target to compile for (qcom or chestnut)' + required: false + type: string + default: 'qcom' + hf_repo: + description: 'Hugging Face dataset repository (e.g. sunnypilot/sunnypilot_models_v1)' + required: false + type: string + default: 'sunnypilot/sunnypilot_models_v1' + set_min_version: + description: 'Minimum selector version' + required: false + type: string + tinygrad_ref: + description: 'Tinygrad reference' + required: false + type: string + workflow_dispatch: + inputs: + upstream_branch: + description: 'Upstream commit to build from' + required: true + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: true + recompiled_dir: + description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' + required: true + type: string + json_version: + description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' + required: true + type: string + model_folder: + description: 'Model folder' + type: choice + default: 'None' + options: + - None + - Master Models + - Release Models + - 2026 World Models + - 2026 Deep RL Models + - Custom Merge Models + - Other + custom_model_folder: + description: 'Custom model folder name (if "Other" selected)' + required: false + type: string + generation: + description: 'Model generation' + required: false + type: string + version: + description: 'Minimum selector version' + required: false + type: string + target_hardware: + description: 'Hardware target to compile for' + required: false + type: choice + default: 'qcom' + options: + - qcom + - chestnut + hf_repo: + description: 'Hugging Face dataset repository' + required: false + type: string + default: 'sunnypilot/sunnypilot_models_v1' +env: + RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} + JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json + +jobs: + build_model: + uses: ./.github/workflows/sunnypilot-build-model.yaml + with: + upstream_branch: ${{ inputs.upstream_branch }} + custom_name: ${{ inputs.custom_name || inputs.upstream_branch }} + is_20hz: ${{ inputs.is_20hz }} + artifact_suffix: ${{ inputs.artifact_suffix }} + target_hardware: ${{ inputs.target_hardware }} + secrets: inherit + + publish_model: + if: ${{ !cancelled() && needs.build_model.result == 'success' }} + concurrency: + group: hf-push-${{ inputs.recompiled_dir }} + cancel-in-progress: false + needs: build_model + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + steps: + - name: Checkout docs repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-models + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Install huggingface_hub + run: pip install --upgrade "huggingface_hub>=0.22.0" + + - name: Validate hf_repo and JSON version + env: + HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} + run: | + if [ ! -f "$JSON_FILE" ]; then + echo "JSON file $JSON_FILE does not exist!" + exit 1 + fi + python3 -c " + import sys + from huggingface_hub import HfApi + try: + api = HfApi() + api.repo_info(repo_id=sys.argv[1], repo_type='dataset') + print(f'Success: Repo {sys.argv[1]} exists.') + except Exception as e: + print('HF validation failed:', e) + sys.exit(1) + " "${{ inputs.hf_repo }}" + + - name: Download artifact name file + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ inputs.custom_name || inputs.upstream_branch }} + path: artifact_name + + - name: Read artifact name + id: read-artifact-name + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download and extract model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.read-artifact-name.outputs.artifact_name }} + path: output + + - name: Create models folder + env: + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} + run: | + mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" + cp -r output/* "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" + rm -f "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/artifact_name.txt" + + - name: Upload to Hugging Face + env: + HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} + run: | + hf upload ${{ inputs.hf_repo }} \ + output/ \ + "models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \ + --repo-type=dataset + + - name: Pull gh-pages + run: | + cd docs + git pull origin gh-pages + + - name: Run json_parser.py to update JSON + run: | + FOLDER="${{ inputs.model_folder }}" + if [ "$FOLDER" = "Other" ]; then + FOLDER="${{ inputs.custom_model_folder }}" + fi + ARGS="" + if [ "$FOLDER" != "None" ] && [ -n "$FOLDER" ]; then + ARGS="$ARGS --model-folder \"$FOLDER\"" + fi + [ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\"" + [ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\"" + [ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\"" + [ -n "${{ inputs.tinygrad_ref }}" ] && ARGS="$ARGS --tinygrad-ref \"${{ inputs.tinygrad_ref }}\"" + eval python3 docs/json_parser.py \ + --json-path "$JSON_FILE" \ + --recompiled-dir "local_models/$RECOMPILED_DIR" \ + --sort-by-date \ + $ARGS + + - name: Push updated JSON to GitHub docs repo + run: | + cd docs + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git checkout gh-pages + git add docs/"$(basename $JSON_FILE)" + git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit" + git push origin gh-pages diff --git a/.github/workflows/cereal_validation.yaml b/.github/workflows/cereal_validation.yaml new file mode 100644 index 000000000..0c87688bf --- /dev/null +++ b/.github/workflows/cereal_validation.yaml @@ -0,0 +1,97 @@ +name: cereal validation + +on: + push: + branches: + - master + pull_request: + paths: + - 'openpilot/cereal/**' + workflow_dispatch: + workflow_call: + inputs: + run_number: + default: '1' + required: true + type: string + +concurrency: + group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: true + +env: + CI: 1 + +jobs: + validate_cereal_with_upstream: + name: Validate cereal with Upstream + runs-on: ubuntu-24.04 + steps: + - name: Checkout sunnypilot cereal + uses: actions/checkout@v6 + with: + sparse-checkout: | + openpilot/cereal + uv.lock + submodules: false + + - name: Init sunnypilot opendbc submodule + run: git submodule update --init --depth 1 opendbc_repo + + - name: Locate sunnypilot capnp import paths + id: locate-sp-capnp + run: | + SP_IMPORT_ARGS="" + SP_CAR_CAPNP=$(find opendbc_repo -maxdepth 4 -name car.capnp -path '*/opendbc/car/car.capnp' -printf '%h\n' -quit) + if [ -n "$SP_CAR_CAPNP" ]; then + SP_IMPORT_ARGS="-I $SP_CAR_CAPNP" + echo "Found sunnypilot car.capnp at: $SP_CAR_CAPNP" + fi + echo "import_args=$SP_IMPORT_ARGS" >> "$GITHUB_OUTPUT" + + - name: Checkout upstream openpilot + uses: actions/checkout@v6 + with: + repository: 'commaai/openpilot' + path: upstream_openpilot + ref: "refs/heads/master" + + - name: Init upstream opendbc submodule + working-directory: upstream_openpilot + run: git submodule update --init --depth 1 opendbc_repo + + - name: Locate upstream capnp paths + id: locate-capnp + run: | + CEREAL_DIR=$(find upstream_openpilot -maxdepth 4 -name log.capnp -path '*/cereal/log.capnp' -printf '%h\n' -quit) + if [ -z "$CEREAL_DIR" ]; then + echo "::error::Could not locate cereal/log.capnp in upstream openpilot" + exit 1 + fi + echo "cereal_dir=$CEREAL_DIR" >> "$GITHUB_OUTPUT" + echo "Found upstream cereal at: $CEREAL_DIR" + + IMPORT_ARGS="" + CAR_CAPNP=$(find upstream_openpilot -maxdepth 5 -name car.capnp -path '*/opendbc/car/car.capnp' -printf '%h\n' -quit) + if [ -n "$CAR_CAPNP" ]; then + IMPORT_ARGS="-I $CAR_CAPNP" + echo "Found car.capnp at: $CAR_CAPNP" + fi + echo "import_args=$IMPORT_ARGS" >> "$GITHUB_OUTPUT" + + - name: Install pycapnp + run: | + PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))") + pip install "pycapnp==${PYCAPNP_VER}" + + - name: Generate sunnypilot schema + run: | + python3 openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py \ + -g -f /tmp/sp_schema.json --cereal-dir openpilot/cereal \ + ${{ steps.locate-sp-capnp.outputs.import_args }} + + - name: Validate against upstream + run: | + python3 openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py \ + -r -f /tmp/sp_schema.json --cereal-dir ${{ steps.locate-capnp.outputs.cereal_dir }} \ + ${{ steps.locate-capnp.outputs.import_args }} diff --git a/.github/workflows/diff_report.yaml b/.github/workflows/diff_report.yaml new file mode 100644 index 000000000..202bc0ec7 --- /dev/null +++ b/.github/workflows/diff_report.yaml @@ -0,0 +1,58 @@ +name: diff report + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + comment: + name: comment + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pull-requests: write + actions: read + steps: + - name: Wait for process replay + id: wait + continue-on-error: true + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 + with: + ref: ${{ github.event.pull_request.head.sha }} + check-name: process replay + repo-token: ${{ secrets.GITHUB_TOKEN }} + allowed-conclusions: success,failure + wait-interval: 20 + - name: Get tests run ID + if: steps.wait.outcome == 'success' + id: tests-run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + run_id=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/tests.yaml/runs?event=pull_request&head_sha=${{ github.event.pull_request.head.sha }}&per_page=1" \ + --jq '.workflow_runs[0].id // empty') + if [ -z "$run_id" ]; then + echo "No tests.yaml run found for ${{ github.event.pull_request.head.sha }}" >&2 + exit 1 + fi + echo "run-id=$run_id" >> "$GITHUB_OUTPUT" + - name: Download diff + if: steps.wait.outcome == 'success' + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + run-id: ${{ steps.tests-run.outputs.run-id }} + name: diff_report_${{ github.event.number }} + path: . + - name: Comment on PR + if: steps.wait.outcome == 'success' && hashFiles('diff_report.txt') != '' + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b + with: + file-path: diff_report.txt + comment-tag: diff_report + pr-number: ${{ github.event.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 000000000..5c1a8f4dd --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,69 @@ +name: docs + +on: + push: + branches: + - master + pull_request: + workflow_call: + inputs: + run_number: + default: '1' + required: true + type: string +concurrency: + group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: true + +env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + +jobs: + docs: + name: build docs + runs-on: ubuntu-24.04 + steps: + - uses: commaai/timeout@v1 + + - uses: actions/checkout@v7 + with: + submodules: true + + # Build + - name: Build docs + run: | + git lfs pull + python docs/serve.py --build + + # Push to docs.comma.ai + - uses: actions/checkout@v7 + if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' + with: + path: openpilot-docs + ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }} + repository: sunnypilot/sunnypilot-docs + - name: Push + if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' + run: | + set -x + + source tools/release/identity.sh + + cd openpilot-docs + git checkout --orphan tmp + git rm -rf . + + # copy over docs + cp -r ../docs/_site/ docs/ + + # GitHub pages config + touch docs/.nojekyll + echo -n docs.comma.ai > docs/CNAME + + git add -f . + git commit -m "build docs" + + # docs live in different repo to not bloat openpilot's full clone size + git push -f origin tmp:gh-pages diff --git a/.github/workflows/download-hf-model-chunks/action.yml b/.github/workflows/download-hf-model-chunks/action.yml new file mode 100644 index 000000000..01ab5385d --- /dev/null +++ b/.github/workflows/download-hf-model-chunks/action.yml @@ -0,0 +1,66 @@ +name: Download HF model chunks +description: Resolve and download model chunks from HuggingFace in parallel + +inputs: + hf_repo: + description: HuggingFace dataset repo + required: true + models: + description: 'JSON array of {hf_path, onnx_hash, canonical} objects' + required: true + dest_dir: + description: Destination directory for downloaded chunks + required: true + +runs: + using: composite + steps: + - name: Download model chunks + shell: bash + env: + HF_REPO: ${{ inputs.hf_repo }} + MODELS_JSON: ${{ inputs.models }} + DEST_DIR: ${{ inputs.dest_dir }} + run: | + set -eo pipefail + DOWNLOAD_LIST=$(mktemp) + + resolve_chunks() { + local HF_PATH="$1" ONNX_HASH="$2" CANONICAL="$3" DEST_DIR="$4" + local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_PATH}/default_models.json" + local DEFAULTS BUNDLE ARTIFACT BASE_URL NUM_CHUNKS + DEFAULTS=$(curl -fsSL "$JSON_URL") + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + 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') + + mkdir -p "$DEST_DIR" + while IFS= read -r CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true) + if [ -z "$CHUNK_IDX" ]; then + echo "::error::Failed to parse chunk index from: $CHUNK_NAME" + return 1 + fi + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST" + done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name') + echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest" + } + + echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do + HF_PATH=$(echo "$model" | jq -r '.hf_path') + ONNX_HASH=$(echo "$model" | jq -r '.onnx_hash') + CANONICAL=$(echo "$model" | jq -r '.canonical') + resolve_chunks "$HF_PATH" "$ONNX_HASH" "$CANONICAL" "$DEST_DIR" + done + + TOTAL=$(wc -l < "$DOWNLOAD_LIST") + echo "Downloading $TOTAL chunks with 8 parallel connections..." + xargs -P8 -d'\n' -I{} bash -c ' + URL="${1%% *}" + DEST="${1#* }" + echo "Downloading $(basename "$DEST")" + curl -fsSL --retry 3 --retry-delay 5 -o "$DEST" "$URL" + ' _ {} < "$DOWNLOAD_LIST" + rm -f "$DOWNLOAD_LIST" diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml new file mode 100644 index 000000000..ebb14d5db --- /dev/null +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -0,0 +1,96 @@ +name: jenkins scan + +on: + issue_comment: + types: [created, edited] + +jobs: + cleanup-branches: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Delete stale Jenkins branches + uses: actions/github-script@v9 + with: + script: | + const cutoff = Date.now() - 24 * 60 * 60 * 1000; + const prefixes = ['tmp-jenkins', '__jenkins']; + + for await (const response of github.paginate.iterator(github.rest.repos.listBranches, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + })) { + for (const branch of response.data) { + if (!prefixes.some(p => branch.name.startsWith(p))) continue; + + const { data: commit } = await github.rest.repos.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: branch.commit.sha, + }); + + const commitDate = new Date(commit.commit.committer.date).getTime(); + if (commitDate < cutoff) { + console.log(`Deleting branch: ${branch.name} (last commit: ${commit.commit.committer.date})`); + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `heads/${branch.name}`, + }); + } + } + } + + scan-comments: + runs-on: ubuntu-latest + if: ${{ github.event.issue.pull_request }} + permissions: + contents: write + issues: write + steps: + - name: Check for trigger phrase + id: check_comment + uses: actions/github-script@v9 + with: + script: | + const triggerPhrase = "trigger-jenkins"; + const comment = context.payload.comment.body; + const commenter = context.payload.comment.user.login; + + const { data: permissions } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commenter + }); + + const hasWriteAccess = permissions.permission === 'write' || permissions.permission === 'admin'; + + return (hasWriteAccess && comment.includes(triggerPhrase)); + result-encoding: json + + - name: Checkout repository + if: steps.check_comment.outputs.result == 'true' + uses: actions/checkout@v7 + with: + ref: refs/pull/${{ github.event.issue.number }}/head + + - name: Push to tmp-jenkins branch + if: steps.check_comment.outputs.result == 'true' + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b tmp-jenkins-${{ github.event.issue.number }} + GIT_LFS_SKIP_PUSH=1 git push -f origin tmp-jenkins-${{ github.event.issue.number }} + + - name: Delete trigger comment + if: steps.check_comment.outputs.result == 'true' && always() + uses: actions/github-script@v9 + with: + script: | + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + }); diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml new file mode 100644 index 000000000..3b92996f8 --- /dev/null +++ b/.github/workflows/lfs-maintenance.yaml @@ -0,0 +1,85 @@ +name: Sync comma's LFS + +env: + LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' + LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' + +on: + schedule: + - cron: '0 0 * * *' # Runs at 00:00 UTC every day + push: + branches: + - 'master' + pull_request: + branches: + - 'master' + workflow_dispatch: # enables manual triggering + inputs: + upstream_branch: + default: 'master' + type: string + +jobs: + sync: + runs-on: ubuntu-latest + # Skip if PR is in draft mode + if: (github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)) && !github.event.pull_request.head.repo.fork + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + repository: 'commaai/openpilot' + ref: ${{ inputs.upstream_branch }} + + - name: LFS Fetch + run: | + git lfs fetch + + - name: Set up Git + run: | + git config --global user.name 'GitHub Action' + git config --global user.email 'action@github.com' + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add GitLab public keys + run: | + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Ensure branch + run: | + if git symbolic-ref -q HEAD >/dev/null; then + echo "Already on a branch, proceeding with push" + else + echo "Detached HEAD state detected, creating temporary branch" + git checkout -b temp_branch + fi + + - name: Update LFS Config + run: | + echo '[lfs]' > .lfsconfig + echo ' url = ${{ env.LFS_URL }}' >> .lfsconfig + echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig + echo ' locksverify = false' >> .lfsconfig + + - name: Configure LFS transfer settings + run: | + git config lfs.activitytimeout 300 + git config lfs.transfer.maxretries 5 + git config lfs.concurrenttransfers 4 + + - name: Push LFS + id: sync-and-commit + run: | + git lfs ls-files -l + for attempt in 1 2 3; do + echo "Push attempt $attempt..." + git lfs push --all origin && exit 0 + echo "Attempt $attempt failed, retrying in 30s..." + sleep 30 + done + echo "All push attempts failed" + exit 1 diff --git a/.github/workflows/post-to-discourse/action.yml b/.github/workflows/post-to-discourse/action.yml new file mode 100644 index 000000000..55232ce0e --- /dev/null +++ b/.github/workflows/post-to-discourse/action.yml @@ -0,0 +1,105 @@ +name: 'Post to Discourse' +description: 'Posts a message to a Discourse topic (existing or new)' + +inputs: + discourse-url: + description: 'Discourse instance URL (e.g., https://discourse.example.com)' + required: true + api-key: + description: 'Discourse API key' + required: true + api-username: + description: 'Discourse API username' + required: true + topic-id: + description: 'Discourse topic ID to post to (use this OR category-id + title)' + required: false + category-id: + description: 'Category ID for new topic (required if topic-id not provided)' + required: false + title: + description: 'Title for new topic (required if topic-id not provided)' + required: false + message: + description: 'Message content (markdown supported)' + required: true + +outputs: + post-number: + description: 'The post number in the topic' + value: ${{ steps.post.outputs.post_number }} + post-url: + description: 'Direct URL to the post' + value: ${{ steps.post.outputs.post_url }} + topic-id: + description: 'The topic ID (useful when creating a new topic)' + value: ${{ steps.post.outputs.topic_id }} + +runs: + using: "composite" + steps: + - name: Post to Discourse + id: post + shell: bash + run: | + # Validate inputs + if [ -z "${{ inputs.topic-id }}" ] && ([ -z "${{ inputs.category-id }}" ] || [ -z "${{ inputs.title }}" ]); then + echo "❌ Error: Must provide either topic-id OR both category-id and title" + exit 1 + fi + + if [ -n "${{ inputs.topic-id }}" ] && ([ -n "${{ inputs.category-id }}" ] || [ -n "${{ inputs.title }}" ]); then + echo "⚠️ Warning: Both topic-id and category-id/title provided. Will post to existing topic." + fi + + # Determine if creating new topic or posting to existing + if [ -n "${{ inputs.topic-id }}" ]; then + echo "📝 Posting to existing topic ID: ${{ inputs.topic-id }}" + + # Create JSON payload for posting to existing topic + PAYLOAD=$(jq -n \ + --arg content '${{ inputs.message }}' \ + --arg topic_id "${{ inputs.topic-id }}" \ + '{topic_id: $topic_id, raw: $content}') + else + echo "✨ Creating new topic: ${{ inputs.title }}" + + # Create JSON payload for new topic + PAYLOAD=$(jq -n \ + --arg content '${{ inputs.message }}' \ + --arg title "${{ inputs.title }}" \ + --arg category "${{ inputs.category-id }}" \ + '{title: $title, category: ($category | tonumber), raw: $content}') + fi + + # Post to Discourse + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST "${{ inputs.discourse-url }}/posts.json" \ + -H "Content-Type: application/json" \ + -H "Api-Key: ${{ inputs.api-key }}" \ + -H "Api-Username: ${{ inputs.api-username }}" \ + -d "$PAYLOAD") + + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "✅ Successfully posted to Discourse!" + + POST_NUMBER=$(echo "$BODY" | jq -r '.post_number // "unknown"') + TOPIC_ID=$(echo "$BODY" | jq -r '.topic_id // "${{ inputs.topic-id }}"') + POST_URL="${{ inputs.discourse-url }}/t/${TOPIC_ID}/${POST_NUMBER}" + + echo "post_number=${POST_NUMBER}" >> $GITHUB_OUTPUT + echo "post_url=${POST_URL}" >> $GITHUB_OUTPUT + echo "topic_id=${TOPIC_ID}" >> $GITHUB_OUTPUT + + echo "Topic ID: ${TOPIC_ID}" + echo "Post number: ${POST_NUMBER}" + echo "URL: ${POST_URL}" + else + echo "❌ Failed to post to Discourse" + echo "HTTP Code: ${HTTP_CODE}" + echo "Response: ${BODY}" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 000000000..c072e98e2 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,28 @@ +name: Release Drafter + +on: + push: + branches: + - master + tags: + - 'v*' + pull_request_target: + types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter.yml + prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..2240713bc --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,36 @@ +name: release +on: + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + +jobs: + build___nightly: + name: build __nightly + runs-on: ubuntu-latest + if: github.repository == 'sunnypilot/sunnypilot' + permissions: + checks: read + contents: write + steps: + - name: Wait for green check mark + if: ${{ github.event_name == 'schedule' }} + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 + with: + ref: master + wait-interval: 30 + running-workflow-name: 'build __nightly' + repo-token: ${{ secrets.GITHUB_TOKEN }} + check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$ + - uses: actions/checkout@v4 # checkout version > v4 breaks nightly release script + with: + submodules: true + fetch-depth: 0 + - run: ./tools/op.sh setup + - name: Push __nightly + run: BRANCH=__nightly tools/release/build_stripped.sh diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml new file mode 100644 index 000000000..731300512 --- /dev/null +++ b/.github/workflows/repo-maintenance.yaml @@ -0,0 +1,132 @@ +name: repo maintenance + +on: + schedule: + - cron: "0 14 * * 1" # every Monday at 2am UTC (6am PST) + pull_request: + types: [closed] + workflow_dispatch: + +env: + PYTHONPATH: ${{ github.workspace }} + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + +jobs: + package_updates: + name: package_updates + runs-on: ubuntu-latest + if: >- + github.repository == 'sunnypilot/sunnypilot' && + (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - run: ./tools/op.sh setup + - name: uv lock + run: uv lock --upgrade + - name: uv pip tree + id: pip_tree + run: | + echo 'PIP_TREE<> $GITHUB_OUTPUT + uv pip tree >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + - name: venv size + id: venv_size + run: | + echo 'VENV_SIZE<> $GITHUB_OUTPUT + echo "Total: $(du -sh .venv | cut -f1)" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "Top 10 by size:" >> $GITHUB_OUTPUT + du -sh .venv/lib/python*/site-packages/* 2>/dev/null \ + | grep -v '\.dist-info' \ + | grep -v '__pycache__' \ + | sort -rh \ + | head -10 \ + | while IFS=$'\t' read size path; do echo "$size ${path##*/}"; done >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + - name: bump submodules + run: | + git config submodule.msgq.update none + git config submodule.rednose_repo.update none + git config submodule.teleoprtc_repo.update none + git config submodule.tinygrad.update none + git submodule update --remote + git add . + - name: update car docs + run: | + python openpilot/selfdrive/car/docs.py + git add docs/CARS.md + - name: Create Pull Request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 + with: + author: github-actions[bot] + token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }} + commit-message: Update Python packages + title: '[bot] Update Python packages' + branch: auto-package-updates + base: master + delete-branch: true + body: | + Automatic PR from repo-maintenance -> package_updates + + ``` + $ du -sh .venv && du -sh .venv/lib/python*/site-packages/* | sort -rh | head -10 + ${{ steps.venv_size.outputs.VENV_SIZE }} + ``` + + ``` + $ uv pip tree + ${{ steps.pip_tree.outputs.PIP_TREE }} + ``` + labels: bot + + cleanup_closed_branches: + if: github.repository == 'commaai/openpilot' && github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/github-script@v9 + with: + script: | + const { owner, repo } = context.repo; + const upstream = `${owner}/${repo}`; + + const closed = context.payload.pull_request; + if (closed) { + if (closed.head.repo?.full_name === upstream) { + await github.rest.git.deleteRef({ owner, repo, ref: `heads/${closed.head.ref}` }).catch(console.log); + } + return; + } + + for await (const response of github.paginate.iterator(github.rest.pulls.list, { + owner, + repo, + state: 'closed', + per_page: 100, + })) { + for (const pr of response.data) { + if (pr.head.repo?.full_name !== upstream) continue; + + const branch = pr.head.ref; + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${branch}`, + }); + console.log(`Deleted branch ${branch} (PR #${pr.number})`); + } catch (error) { + if (error.status === 422 || error.status === 403) { + console.log(`Skipping branch ${branch} (PR #${pr.number}): ${error.message}`); + continue; + } + throw error; + } + } + } diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 000000000..dab61bf3b --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,52 @@ +name: stale +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +env: + DAYS_BEFORE_PR_CLOSE: 7 + DAYS_BEFORE_PR_STALE: 24 + DAYS_BEFORE_PR_STALE_DRAFT: 30 + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + exempt-all-milestones: true + + # pull request config + stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' + close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' + stale-pr-label: stale + delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'sunnypilot/sunnypilot' }} # only delete branches on the main repo + exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale + days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} + days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-draft-pr: false + + # issue config + days-before-issue-stale: -1 # ignore issues for now + + # same as above, but give draft PRs more time + stale_drafts: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + exempt-all-milestones: true + + # pull request config + stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' + close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' + stale-pr-label: stale + delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo + exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale + days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} + days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-draft-pr: true + + # issue config + days-before-issue-stale: -1 # ignore issues for now diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml new file mode 100644 index 000000000..459fa7459 --- /dev/null +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -0,0 +1,301 @@ +name: Build Model from Upstream + +env: + BUILD_DIR: "/data/openpilot" + OUTPUT_DIR: ${{ github.workspace }}/output + SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache + UPSTREAM_REPO: "commaai/openpilot" + TINYGRAD_PATH: ${{ github.workspace }}/tinygrad_repo + MODELS_DIR: ${{ github.workspace }}/openpilot/selfdrive/modeld/models + +on: + workflow_call: + inputs: + upstream_branch: + description: 'Upstream branch to build from' + required: true + default: 'master' + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: true + artifact_suffix: + description: 'Suffix for artifact name' + required: false + type: string + default: '' + target_hardware: + description: 'Hardware target to compile for (qcom or chestnut)' + required: false + type: string + default: 'qcom' + workflow_dispatch: + inputs: + upstream_branch: + description: 'Upstream branch to build from' + required: true + default: 'master' + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: true + target_hardware: + description: 'Hardware target to compile for' + required: true + type: choice + options: + - qcom + - chestnut + default: 'qcom' + + +run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}] + +jobs: + get_model: + runs-on: ubuntu-latest + env: + REF: ${{ inputs.upstream_branch }} + outputs: + model_date: ${{ steps.commit-date.outputs.model_date }} + steps: + # Note: To allow dynamic models from both openpilot and sunnypilot (merges/mashups), we try commaai as default, + # and fallback to sunnypilot if the ref checkout fails. + - name: Checkout commaai/openpilot + id: checkout_upstream + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: commaai/openpilot + ref: ${{ inputs.upstream_branch }} + fetch-depth: 1 + submodules: recursive + path: openpilot + + - name: Fallback to sunnypilot/sunnypilot + if: steps.checkout_upstream.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot + ref: ${{ inputs.upstream_branch }} + fetch-depth: 1 + submodules: recursive + path: openpilot + - name: Get commit date + id: commit-date + run: | + cd ${{ github.workspace }}/openpilot/openpilot + commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') + echo "model_date=${commit_date}" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + - run: | + cd ${{ github.workspace }}/openpilot/openpilot + if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then + git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx" + rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx + else + git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X "" + find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete + fi + if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then + echo "::error::the ONNX files above are still LFS pointers, not real models" + exit 1 + fi + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: models-${{ env.REF }}${{ inputs.artifact_suffix }} + path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx + if-no-files-found: error + + build_model: + runs-on: [self-hosted, chestnut] + needs: get_model + env: + MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) + REF: ${{ inputs.upstream_branch }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: recursive + + + - name: Set environment variables + id: set-env + run: | + # Set up common environment + source /etc/profile; + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + if [[ "${{ runner.debug }}" == "1" ]]; then + cat $GITHUB_OUTPUT + fi + + - name: Setup build environment + run: | + mkdir -p "${BUILD_DIR}/" + sudo find $BUILD_DIR/ -mindepth 1 -delete + echo "Starting build stage..." + echo "BUILD_DIR: ${BUILD_DIR}" + echo "CI_DIR: ${CI_DIR}" + echo "VERSION: ${{ steps.set-env.outputs.version }}" + echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" + echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" + echo "-------" + if [[ "${{ runner.debug }}" == "1" ]]; then + printenv + fi + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + rm -rf ${{ env.MODELS_DIR }}/*.onnx* + + - name: Download model artifacts + uses: actions/download-artifact@v4 + with: + name: models-${{ env.REF }}${{ inputs.artifact_suffix }} + path: ${{ env.MODELS_DIR }} + + - name: Build Model + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ env.TINYGRAD_PATH }}:${{ github.workspace }}" + + COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py" + MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") + CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") + + TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then + echo "CHESTNUT build" + export CHESTNUT=1 + TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" + OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" + else + echo "QCOM build" + TG_FLAGS="$TG_FLAGS_QCOM" + OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl" + fi + + # Generate metadata for all ONNX files + find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do + echo "Generating metadata: $onnx_file" + env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true + done + + # Detect model type and build compile args + VISION_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do + [ -f "$f" ] && VISION_ONNX="$f" && break + done + + POLICY_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do + [ -f "$f" ] && POLICY_ONNX="$f" && break + done + + OFF_POLICY_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do + [ -f "$f" ] && OFF_POLICY_ONNX="$f" && break + done + + ON_POLICY_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do + [ -f "$f" ] && ON_POLICY_ONNX="$f" && break + done + + SUPERCOMBO_ONNX="" + for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do + [ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break + done + + MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME="" + if [ -f "$VISION_ONNX" ]; then + ONNX_ARGS="--vision-onnx $VISION_ONNX" + if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then + MODEL_TYPE=vision_multi_policy + ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX" + elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then + MODEL_TYPE=vision_multi_policy + ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX" + elif [ -f "$POLICY_ONNX" ]; then + MODEL_TYPE=vision_policy + ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX" + fi + elif [ -f "$SUPERCOMBO_ONNX" ]; then + MODEL_TYPE=supercombo + ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX" + fi + + if [ -n "$MODEL_TYPE" ]; then + echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL" + env ${TG_FLAGS} python3 "$COMPILE_MODELD" \ + --model-type $MODEL_TYPE \ + --model-size $MODEL_SIZE \ + --camera-resolutions $CAMERA_RES \ + $ONNX_ARGS \ + --output "$OUTPUT_PKL" + fi + + - name: Prepare Output + run: | + sudo rm -rf ${{ env.OUTPUT_DIR }} + mkdir -p ${{ env.OUTPUT_DIR }} + + # Copy the model files + rsync -avm \ + --include='*.dlc' \ + --include='*.chunk*' \ + --include='*.chunkmanifest' \ + --exclude='*' \ + --delete-excluded \ + --chown=comma:comma \ + ${{ env.MODELS_DIR }}/ ${{ env.OUTPUT_DIR }}/ + + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --model-dir "${{ env.MODELS_DIR }}" \ + --output-dir "${{ env.OUTPUT_DIR }}" \ + --custom-name "${{ env.MODEL_NAME }}" \ + --upstream-branch "${{ inputs.upstream_branch }}" \ + ${{ inputs.is_20hz && '--is-20hz' || '' }} + + - name: Write artifact name to file + run: echo "model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }}" > ${{ env.OUTPUT_DIR }}/artifact_name.txt + + - name: Upload Build Artifacts + id: upload-artifact + uses: actions/upload-artifact@v4 + with: + name: model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }} + path: ${{ env.OUTPUT_DIR }} + + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ inputs.custom_name || inputs.upstream_branch }} + path: ${{ env.OUTPUT_DIR }}/artifact_name.txt + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml new file mode 100644 index 000000000..c93dbe02c --- /dev/null +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -0,0 +1,604 @@ +name: sunnypilot prebuilt action + +env: + BUILD_DIR: "/data/openpilot" + OUTPUT_DIR: ${{ github.workspace }}/output + CI_DIR: ${{ github.workspace }}/release/ci + PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" + + # Runtime configuration + SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" + +on: + push: + branches: [ master, master-dev ] + tags: [ 'release/*' ] + pull_request_target: + types: [ labeled ] + workflow_dispatch: + inputs: + wait_for_tests: + description: 'Wait for tests to finish' + required: false + type: boolean + default: false + +jobs: + prepare_strategy: + runs-on: ubuntu-24.04 + if: (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + outputs: + environment: ${{ steps.strategy.outputs.environment }} + new_branch: ${{ steps.strategy.outputs.new_branch }} + extra_version_identifier: ${{ steps.strategy.outputs.extra_version_identifier }} + version: ${{ steps.strategy.outputs.version }} + cancel_publish_in_progress: ${{ steps.strategy.outputs.cancel_publish_in_progress }} + publish_concurrency_group: ${{ steps.strategy.outputs.publish_concurrency_group }} + is_stable_branch: ${{ steps.strategy.outputs.is_stable_branch }} + build: ${{ steps.strategy.outputs.build }} + include_big_model: ${{ steps.strategy.outputs.include_big_model }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Extract deploy strategy + id: strategy + run: | + echo '::group::Strategy Extraction' + BRANCH="${{ github.head_ref || github.ref_name }}" + echo "Current branch: $BRANCH" + + STRATEGY_JSON='${{ vars.DEPLOY_STRATEGY }}' + CONFIG=$(echo "$STRATEGY_JSON" | jq -r --arg branch "$BRANCH" ' + .configs[] | select(.branch == $branch) + ') + + BUILD="$(date '+%Y.%m.%d')-${{ github.run_number }}" + if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then + echo "No exact strategy match found. Falling back to feature/fork logic." + IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}" + FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" ) + NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt" + + echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT + echo "version=$BUILD" >> $GITHUB_OUTPUT + echo "cancel_publish_in_progress=true" >> $GITHUB_OUTPUT + echo "publish_concurrency_group=publish-${BRANCH}" >> $GITHUB_OUTPUT + echo "environment=feature-branch" >> $GITHUB_OUTPUT + echo "extra_version_identifier=feature-branch" >> $GITHUB_OUTPUT + else + echo "Matched config: $CONFIG" + environment=$(echo "$CONFIG" | jq -r '.environment') + echo "environment=$environment" >> $GITHUB_OUTPUT + echo "new_branch=$(echo "$CONFIG" | jq -r '.target_branch')" >> $GITHUB_OUTPUT + cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')"; + echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT + echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT + + is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')"; + echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT + + stable_version=$(cat openpilot/sunnypilot/common/version.h | grep SUNNYPILOT_VERSION | sed -e 's/[^0-9|.]//g'); + echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT + echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT + + include_big_model="$(echo "$CONFIG" | jq -r '.include_big_model // false')"; + echo "include_big_model=$include_big_model" >> $GITHUB_OUTPUT + fi + echo "build=$BUILD" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + validate_tests: + runs-on: ubuntu-24.04 + needs: [ prepare_strategy ] + if: ${{ + ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || + (github.event_name == 'push' && needs.prepare_strategy.outputs.is_stable_branch == 'true') || + contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Wait for Tests + uses: ./.github/workflows/wait-for-action # Path to where you place the action + with: + workflow: tests.yaml # The workflow file to monitor + github-token: ${{ secrets.GITHUB_TOKEN }} + should-wait-for-start: ${{ github.event_name == 'push' && 'true' || 'false' }} + + build: + needs: [ validate_tests, prepare_strategy ] + concurrency: + group: build-${{ github.head_ref || github.ref_name }} + cancel-in-progress: false + runs-on: [self-hosted, tici] + if: ${{ + (always() && !cancelled() && !failure()) && + needs.prepare_strategy.result == 'success' && + (needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') && + (!contains(github.event_name, 'pull_request') || + (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: recursive + ref: ${{ env.SOURCE_BRANCH }} + repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }} + - run: git lfs pull + + - name: Set environment variables + run: | + # Set up common environment + source /etc/profile; + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync + printenv >> $GITHUB_ENV + + - name: Setup build environment + run: | + mkdir -p "${BUILD_DIR}/" + sudo find $BUILD_DIR/ -mindepth 1 -delete + echo "Starting build stage..." + echo "BUILD_DIR: ${BUILD_DIR}" + echo "CI_DIR: ${CI_DIR}" + echo "VERSION: ${{ needs.prepare_strategy.outputs.version }}" + echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" + echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" + echo "-------" + if [[ "${{ runner.debug }}" == "1" ]]; then + printenv + fi + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Build Main Project + run: | + export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" + ./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$BUILD_DIR" -- + # outside the checkout, which is wiped each run. /data/scons_cache is the device's, not ours. + SCONS_CACHE="$RUNNER_WORKSPACE/scons_cache" + mkdir -p "$SCONS_CACHE" + cd $BUILD_DIR + echo "Building locationd..." + # -j1: parallel rednose generators OOM the device + scons -j1 cache_dir="$SCONS_CACHE" --minimal \ + openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd + echo "Building rest of sunnypilot" + SKIP_TINYGRAD_COMPILE=1 /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal + touch ${BUILD_DIR}/prebuilt + if [[ "${{ runner.debug }}" == "1" ]]; then + ls -la ${BUILD_DIR} + fi + + - name: Strip release tree + run: | + cd $BUILD_DIR + find . -name '*.a' -delete + find . -name '*.o' -delete + find . -name '*.os' -delete + find . -name '*.pyc' -delete + find . -name 'moc_*' -delete + find . -name '__pycache__' -type d -exec rm -rf {} + + find . -name 'SConstruct' -delete + find . -name 'SConscript' -delete + rm -rf .sconsign.dblite Jenkinsfile tools/release/ release/ + rm -f openpilot/selfdrive/modeld/models/*.onnx* + rm -f openpilot/sunnypilot/modeld*/models/*.onnx* + find openpilot/third_party/ -name '*x86*' -exec rm -r {} + + find openpilot/third_party/ -name '*Darwin*' -exec rm -r {} + + cd - + + - name: 'Tar.gz files' + run: | + tar czf prebuilt.tar.gz -C ${{ env.BUILD_DIR }} . + ls -la prebuilt.tar.gz + + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: prebuilt + path: prebuilt.tar.gz + compression-level: 0 + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + + prepare_chestnut: + 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: + GH_REPO: ${{ github.repository }} + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/big + steps: + - name: Resolve ONNX hash and tinygrad ref via API + id: resolve + run: | + REF="${{ github.head_ref || github.ref_name }}" + + ONNX_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + echo "ONNX hash: $ONNX_HASH" + echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT + + TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') + echo "tinygrad ref: $TINYGRAD_REF" + + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + + check_defaults() { + DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) + [ "$TINYGRAD_MATCH" = "true" ] || return 1 + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + [ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ] + } + + if check_defaults; then + echo "HF defaults match repo ONNX hash and tinygrad ref" + exit 0 + fi + + echo "No matching model on HF — dispatching build" + gh workflow run build-default-models.yaml --ref "$REF" -f target=big + + echo "Polling HF for big model availability..." + for i in $(seq 1 90); do + sleep 30 + if check_defaults; then + echo "Big model available on HF after $((i * 30))s" + exit 0 + fi + echo "Poll $i/90: not yet available" + done + + echo "::error::Big model not available on HF after 45 minutes" + exit 1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cancel run on failure + if: failure() + run: gh run cancel ${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + prepare_small_model: + needs: [ prepare_strategy ] + runs-on: ubuntu-24.04 + outputs: + driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }} + env: + GH_REPO: ${{ github.repository }} + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/small + steps: + - name: Resolve ONNX hash and tinygrad ref via API + id: resolve + run: | + REF="${{ github.head_ref || github.ref_name }}" + + DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + echo "Driving ONNX hash: $DRIVING_HASH" + echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT + + TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') + echo "tinygrad ref: $TINYGRAD_REF" + + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + + check_defaults() { + DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) + [ "$TINYGRAD_MATCH" = "true" ] || return 1 + DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + [ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] || return 1 + } + + if check_defaults; then + echo "HF defaults match repo ONNX hash and tinygrad ref" + exit 0 + fi + + echo "No matching model on HF — dispatching build" + gh workflow run build-default-models.yaml --ref "$REF" -f target=small + + echo "Polling HF for model availability..." + for i in $(seq 1 60); do + sleep 30 + if check_defaults; then + echo "Model available on HF after $((i * 30))s" + exit 0 + fi + echo "Poll $i/60: not yet available" + done + + echo "::error::Small driving model not available on HF after 30 minutes" + exit 1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cancel run on failure + if: failure() + run: gh run cancel ${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + prepare_dm_model: + needs: [ prepare_strategy ] + runs-on: ubuntu-24.04 + outputs: + dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} + env: + GH_REPO: ${{ github.repository }} + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/dm + steps: + - name: Resolve ONNX hash and tinygrad ref via API + id: resolve + run: | + REF="${{ github.head_ref || github.ref_name }}" + + DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + echo "DM ONNX hash: $DM_HASH" + echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT + + TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') + echo "tinygrad ref: $TINYGRAD_REF" + + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + + check_defaults() { + DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) + [ "$TINYGRAD_MATCH" = "true" ] || return 1 + DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + [ -n "$DM" ] && [ "$DM" != "null" ] || return 1 + } + + if check_defaults; then + echo "HF defaults match DM ONNX hash and tinygrad ref" + exit 0 + fi + + echo "No matching DM model on HF — dispatching build" + gh workflow run build-default-models.yaml --ref "$REF" -f target=dm + + echo "Polling HF for DM model availability..." + for i in $(seq 1 60); do + sleep 30 + if check_defaults; then + echo "DM model available on HF after $((i * 30))s" + exit 0 + fi + echo "Poll $i/60: not yet available" + done + + echo "::error::DM model not available on HF after 30 minutes" + exit 1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cancel run on failure + if: failure() + run: gh run cancel ${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish: + concurrency: + group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }} + cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }} + if: ${{ + always() && !cancelled() && + needs.build.result == 'success' && + needs.prepare_strategy.result == 'success' && + needs.prepare_small_model.result == 'success' && + needs.prepare_dm_model.result == 'success' && + (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) && + (needs.prepare_strategy.outputs.include_big_model != 'true' || needs.prepare_chestnut.result == 'success') + }} + needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ] + runs-on: ubuntu-24.04 + environment: ${{ needs.prepare_strategy.outputs.environment }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download prebuilt artifact + uses: actions/download-artifact@v4 + with: + name: prebuilt + + - name: Untar prebuilt + run: | + mkdir -p ${{ env.OUTPUT_DIR }} + tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} + + - name: Download model chunks from HF + uses: ./.github/workflows/download-hf-model-chunks + with: + hf_repo: sunnypilot/sunnypilot_models_v1 + dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models + models: | + [ + {"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"}, + {"hf_path": "models/defaults/dm", "onnx_hash": "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}", "canonical": "dmonitoring_model_tinygrad.pkl"} + ] + + - name: Configure Git + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Publish to Public Repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo '${{ toJSON(needs.prepare_strategy.outputs) }}' + ls -la ${{ env.OUTPUT_DIR }} + + ${{ env.CI_DIR }}/publish.sh \ + "${{ github.workspace }}" \ + "${{ env.OUTPUT_DIR }}" \ + "${{ needs.prepare_strategy.outputs.new_branch }}" \ + "${{ needs.prepare_strategy.outputs.version }}" \ + "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ + "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" + + - name: Tag ${{ needs.prepare_strategy.outputs.environment }} + if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }} + run: | + TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}" + git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.prepare_strategy.outputs.build }}." + git push -f origin ${TAG} + + publish_chestnut: + concurrency: + group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }}-chestnut + cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }} + if: ${{ + always() && !cancelled() && + needs.build.result == 'success' && + needs.prepare_strategy.result == 'success' && + needs.prepare_small_model.result == 'success' && + needs.prepare_dm_model.result == 'success' && + needs.prepare_chestnut.result == 'success' && + (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + }} + needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ] + runs-on: ubuntu-24.04 + environment: ${{ needs.prepare_strategy.outputs.environment }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download prebuilt artifact + uses: actions/download-artifact@v4 + with: + name: prebuilt + + - name: Untar prebuilt + run: | + mkdir -p ${{ env.OUTPUT_DIR }} + tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} + + - name: Download model chunks from HF + uses: ./.github/workflows/download-hf-model-chunks + with: + hf_repo: sunnypilot/sunnypilot_models_v1 + dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models + models: | + [ + {"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"}, + {"hf_path": "models/defaults/dm", "onnx_hash": "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}", "canonical": "dmonitoring_model_tinygrad.pkl"}, + {"hf_path": "models/defaults/big", "onnx_hash": "${{ needs.prepare_chestnut.outputs.onnx_sha256 }}", "canonical": "big_driving_tinygrad.pkl"} + ] + + - name: Configure Git + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Publish chestnut branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut" + + ${{ env.CI_DIR }}/publish.sh \ + "${{ github.workspace }}" \ + "${{ env.OUTPUT_DIR }}" \ + "$CHESTNUT_BRANCH" \ + "${{ needs.prepare_strategy.outputs.version }}" \ + "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ + "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" + + notify: + needs: + - prepare_strategy + - build + - publish + - publish_chestnut + - prepare_chestnut + - prepare_small_model + - prepare_dm_model + runs-on: ubuntu-24.04 + if: ${{ (always() && !cancelled() && !failure()) + && needs.publish.result == 'success' + && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + && (fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES_V2)[github.head_ref || github.ref_name] != null) }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Prepare notification message + id: message + run: | + export VERSION="${{ needs.prepare_strategy.outputs.version }}" + export branch_name="${{ env.SOURCE_BRANCH }}" + export new_branch="${{ needs.prepare_strategy.outputs.new_branch }}" + export commit_sha="${{ github.sha }}" + export commit_short_sha="${{ github.sha }}" + export commit_short_sha="${commit_short_sha:0:7}" + export extra_version_identifier="${{ needs.prepare_strategy.outputs.extra_version_identifier || github.run_number }}" + export PUBLIC_REPO_URL="${{ env.PUBLIC_REPO_URL }}" + export chestnut_branch="${{ needs.prepare_chestnut.result == 'success' && format('{0}-chestnut', needs.prepare_strategy.outputs.new_branch) || '' }}" + + MESSAGE=$(cat << 'EOF' | envsubst + ${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }} + EOF + ) + + { + echo 'content<> $GITHUB_OUTPUT + shell: bash + + - name: Post to Discourse + uses: ./.github/workflows/post-to-discourse + with: + discourse-url: ${{ vars.DISCOURSE_URL }} + api-key: ${{ secrets.DISCOURSE_API_KEY }} + api-username: "system" + topic-id: ${{ fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES_V2)[github.head_ref || github.ref_name].topic_id }} + message: ${{ steps.message.outputs.content }} + + manage-pr-labels: + name: Remove prebuilt label + runs-on: ubuntu-latest + if: (always() && contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + env: + LABEL: prebuilt + steps: + - name: Remove trust-fork-pr label if present + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: process.env.LABEL + }); + + console.log(`Removed '${process.env.LABEL}' label from PR #${prNumber}`); diff --git a/.github/workflows/sunnypilot-master-dev-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml new file mode 100644 index 000000000..1cb574764 --- /dev/null +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -0,0 +1,243 @@ +name: Build dev + +env: + DEFAULT_SOURCE_BRANCH: "master" + DEFAULT_TARGET_BRANCH: "master-dev" + LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' + LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' + +on: + push: + branches: + - master + pull_request_target: + types: [ labeled ] + branches: + - 'master' + workflow_dispatch: + inputs: + source_branch: + description: 'Source branch to reset from' + required: true + default: 'master' + type: string + target_branch: + description: 'Target branch to reset and squash into' + required: true + default: 'master-dev' + type: string + cancel_in_progress: + description: 'Cancel any in-progress runs of this workflow' + required: false + default: true + type: boolean + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: ${{ inputs.cancel_in_progress || github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + +jobs: + reset-and-squash: + runs-on: ubuntu-latest + if: ( + (github.event_name == 'workflow_dispatch') + || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == vars.PREBUILT_PR_LABEL || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, vars.PREBUILT_PR_LABEL)))) + ) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches + token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false + + - name: Wait for Tests + uses: ./.github/workflows/wait-for-action # Path to where you place the action + if: ( + (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == vars.PREBUILT_PR_LABEL || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, vars.PREBUILT_PR_LABEL)))) + ) + with: + workflow: tests.yaml # The workflow file to monitor + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add GitLab public keys + run: | + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install PyGithub + + - name: Check branches exist + run: | + # Check if source branch exists + if ! git ls-remote --heads origin ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} | grep -q "${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}"; then + echo "Source branch ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} does not exist!" + exit 1 + fi + + # Make sure we have the latest source branch + git fetch origin ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} + + # Check if target branch exists + if ! git ls-remote --heads origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} | grep -q "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"; then + echo "Target branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} does not exist, creating it from ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" + git checkout -b ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} origin/${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} + git push origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} + else + # Fetch target branch if it exists + git fetch origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} + fi + + - name: Reset target branch + run: | + echo "Resetting ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} to match ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" + # Delete if exists and recreate pointing to source + git branch -D ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} || true + git branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} origin/${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} + + - name: Get PRs to squash + id: get-prs + run: | + # Use GitHub API to get PRs with specific label, ordered by creation date + PR_LIST=$(gh api graphql -f query=' + query($search_query:String!) { + search(query: $search_query, type:ISSUE, first:40) { + nodes { + ... on PullRequest { + number + headRefName + title + createdAt + labels(last:10) { + nodes { + name + } + } + headRepository { + name + nameWithOwner + url + isFork + } + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } + } + } + } + }' -F search_query="repo:${{ github.repository }} is:pr is:open label:${{ vars.PREBUILT_PR_LABEL }},${{ vars.PREBUILT_PR_LABEL }}-c3 draft:false sort:created-asc") + + PR_LIST=${PR_LIST//\'/} + echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Process PRs + run: | + cp ${{ github.workspace }}/release/ci/squash_and_merge.py /tmp/squash_and_merge.py && \ + chmod +x /tmp/squash_and_merge.py && \ + python3 ${{ github.workspace }}/release/ci/squash_and_merge_prs.py \ + --pr-data '${{ steps.get-prs.outputs.PR_LIST }}' \ + --target-branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} \ + --squash-script-path '/tmp/squash_and_merge.py' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update LFS Config + run: | + echo '[lfs]' > .lfsconfig + echo ' url = ${{ env.LFS_URL }}' >> .lfsconfig + echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig + echo ' locksverify = false' >> .lfsconfig + + - name: Restore workflows from source + run: | + TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + SOURCE_BRANCH="${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" + + # Ensure we are on the target branch + git checkout $TARGET_BRANCH + + echo "Restoring .github/workflows from $SOURCE_BRANCH" + git checkout origin/$SOURCE_BRANCH -- .github/workflows + + if ! git diff --cached --quiet; then + echo "Workflows differ. Committing restoration." + git commit -m "chore: restore .github/workflows from $SOURCE_BRANCH" + else + echo "Workflows match $SOURCE_BRANCH." + fi + + - uses: actions/create-github-app-token@v2 + id: ci-token + with: + app-id: ${{ secrets.CI_GITHUB_ACTIONS_TOKEN_APP_ID }} + private-key: ${{ secrets.CI_GITHUB_ACTIONS_TOKEN_APP_PRIVATE_KEY }} + + - name: Push changes if there are diffs + id: push-changes + run: | + TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + + # Use the App Token to set the remote URL with authentication + git remote set-url origin "https://x-access-token:${{ steps.ci-token.outputs.token }}@github.com/${{ github.repository }}.git" + + # Fetch the latest from remote + git fetch origin $TARGET_BRANCH + + # Check for diffs between local and remote + if git diff $TARGET_BRANCH origin/$TARGET_BRANCH --quiet; then + echo "No changes to push - local and remote branches are identical" + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Push with the authenticated origin + if ! git push origin $TARGET_BRANCH --force; then + echo "Failed to push changes to $TARGET_BRANCH" + exit 1 + fi + + echo "Branch $TARGET_BRANCH has been reset and updated with squashed PRs" + echo "has_changes=true" >> $GITHUB_OUTPUT + + - name: Trigger and wait for selfdrive tests + if: steps.push-changes.outputs.has_changes == 'true' + run: | + echo "Triggering selfdrive tests..." + gh workflow run tests.yaml --ref "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + + echo "Sleeping for 120s to give plenty of time for the action to start and then we wait" + sleep 120 + + echo "Getting latest run ID..." + RUN_ID=$(gh run list --workflow=tests.yaml --branch="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" --limit=1 --json databaseId --jq '.[0].databaseId') + + echo "Watching run ID: $RUN_ID" + gh run watch "$RUN_ID" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-discourse.yaml.yml b/.github/workflows/test-discourse.yaml.yml new file mode 100644 index 000000000..fadaec4ea --- /dev/null +++ b/.github/workflows/test-discourse.yaml.yml @@ -0,0 +1,78 @@ +name: Debug Discourse Posting + +on: + push: + +jobs: + test-discourse-post: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Post test message to Discourse + uses: ./.github/workflows/post-to-discourse + with: + discourse-url: ${{ vars.DISCOURSE_URL }} + api-key: ${{ secrets.DISCOURSE_API_KEY }} + api-username: ${{ secrets.DISCOURSE_API_USERNAME }} + topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }} + message: | + ## 🧪 Test Post from GitHub Actions + + **This is a test post to verify Discourse integration** + + - **Workflow**: ${{ github.workflow }} + - **Run Number**: #${{ github.run_number }} + - **Branch**: `${{ github.ref_name }}` + - **Commit**: ${{ github.sha }} + - **Actor**: @${{ github.actor }} + - **Timestamp**: ${{ github.event.head_commit.timestamp }} + + --- + + ### Fake Build Info (for testing) + - **Version**: 0.9.8-test + - **Build**: #42 + - **Branch**: release-test + + [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + *This is an automated test message. Drive safe! 🚗💨* + + + - name: Create topic on Discourse + uses: ./.github/workflows/post-to-discourse + with: + discourse-url: ${{ vars.DISCOURSE_URL }} + api-key: ${{ secrets.DISCOURSE_API_KEY }} + api-username: ${{ secrets.DISCOURSE_API_USERNAME }} + #topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }} + category-id: 4 + title: "This is a test of a new topic instead of a reply" + message: | + ## 🧪 Test Post from GitHub Actions + + **This is a test post to verify Discourse integration** + + - **Workflow**: ${{ github.workflow }} + - **Run Number**: #${{ github.run_number }} + - **Branch**: `${{ github.ref_name }}` + - **Commit**: ${{ github.sha }} + - **Actor**: @${{ github.actor }} + - **Timestamp**: ${{ github.event.head_commit.timestamp }} + + --- + + ### Fake Build Info (for testing) + - **Version**: 0.9.8-test + - **Build**: #42 + - **Branch**: release-test + + [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + *This is an automated test message. Drive safe! 🚗💨* + - name: Display results + if: always() + run: | + echo "::notice::Discourse post test completed" + echo "Check your Discourse topic to verify the post appeared correctly" \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 000000000..b2e409696 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,243 @@ +name: tests + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + workflow_call: + inputs: + run_number: + default: '1' + required: true + type: string + +concurrency: + group: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: true + +env: + CI: 1 + PYTHONPATH: ${{ github.workspace }} + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + +jobs: + build_release: + name: build release + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16"]') + || fromJSON('["ubuntu-24.04"]') }} + env: + STRIPPED_DIR: /tmp/releasepilot + PYTHONPATH: /tmp/releasepilot + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - name: Getting LFS files + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 + with: + timeout_minutes: 2 + max_attempts: 3 + command: git lfs pull + - name: Build devel + timeout-minutes: 3 + run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh + - run: ./tools/op.sh setup + - name: Build openpilot and run checks + timeout-minutes: 30 + working-directory: ${{ env.STRIPPED_DIR }} + run: python3 openpilot/system/manager/build.py + - name: Run tests + timeout-minutes: 1 + working-directory: ${{ env.STRIPPED_DIR }} + run: tools/release/check-dirty.sh + - name: Check submodules + if: github.repository == 'sunnypilot/sunnypilot' + timeout-minutes: 3 + run: | + if [ "${{ github.ref }}" != "refs/heads/master" ]; then + git fetch origin master:refs/remotes/origin/master + + SUBMODULE_PATHS=$(git diff origin/master HEAD --name-only | grep -E '^[^/]+$' | while read path; do + if git ls-files --stage "$path" | grep -q "^160000"; then + echo "$path" + fi + done | tr '\n' ' ') + + if [ -n "$SUBMODULE_PATHS" ]; then + echo "Changed submodule paths: $SUBMODULE_PATHS" + export SUBMODULE_PATHS="$SUBMODULE_PATHS" + export CHECK_PR_REFS=true + fi + fi + tools/release/check-submodules.sh + + build_mac: + name: build macOS + runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} + steps: + - uses: actions/checkout@v7 + - name: Remove Homebrew from environment + run: | + FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':') + echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV + - run: ./tools/op.sh setup + - name: Building openpilot + run: scons + + static_analysis: + name: static analysis + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16"]') + || fromJSON('["ubuntu-24.04"]') }} + steps: + - uses: actions/checkout@v7 + - run: ./tools/op.sh setup + - name: Static analysis + timeout-minutes: 1 + run: scripts/lint/lint.sh + + unit_tests: + name: unit tests + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16"]') + || fromJSON('["ubuntu-24.04"]') }} + steps: + - uses: actions/checkout@v7 + - run: ./tools/op.sh setup + - name: Install raylib headless dependencies + if: ${{ !contains(runner.name, 'nsc') }} + run: sudo apt-get install -y --no-install-recommends libgles2 libegl1 + - name: Build openpilot + run: scons + - name: Run unit tests + timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 999 }} + env: + RAYLIB_BACKEND: headless + run: tools/op.sh test + + process_replay: + name: process replay + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16"]') + || fromJSON('["ubuntu-24.04"]') }} + steps: + - uses: actions/checkout@v7 + - run: ./tools/op.sh setup + - name: Build openpilot + run: scons + - name: Run replay + timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }} + continue-on-error: ${{ github.ref == 'refs/heads/master' }} + run: openpilot/selfdrive/test/process_replay/test_processes.py -j$(nproc) + - name: Print diff + id: print-diff + if: always() + run: cat openpilot/selfdrive/test/process_replay/diff.txt + - name: Print diff report + if: always() + run: cat openpilot/selfdrive/test/process_replay/diff_report.txt + - uses: actions/upload-artifact@v7 + if: always() + continue-on-error: true + with: + name: process_replay_diff.txt + path: openpilot/selfdrive/test/process_replay/diff.txt + - name: Upload diff report + uses: actions/upload-artifact@v7 + if: always() && github.event_name == 'pull_request' + continue-on-error: true + with: + name: diff_report_${{ github.event.number }} + path: openpilot/selfdrive/test/process_replay/diff_report.txt + - name: Checkout ci-artifacts + if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master' + uses: actions/checkout@v7 + with: + repository: sunnypilot/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/ci-artifacts + - name: Prepare refs + if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master' + working-directory: ${{ github.workspace }}/ci-artifacts + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + git fetch origin process-replay || true + git checkout process-replay 2>/dev/null || git checkout --orphan process-replay + cp ${{ github.workspace }}/openpilot/selfdrive/test/process_replay/fakedata/*.zst . + echo "${{ github.sha }}" > ref_commit + git add . + git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit" + - name: Push refs + if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master' + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 + with: + timeout_minutes: 2 + max_attempts: 3 + command: cd ${{ github.workspace }}/ci-artifacts && git push origin process-replay --force + + simulator_driving: + name: simulator driving + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16"]') + || fromJSON('["ubuntu-24.04"]') }} + if: false # FIXME: Started to timeout recently + steps: + - uses: actions/checkout@v7 + - run: ./tools/op.sh setup + - name: Build openpilot + run: scons + - name: Driving test + timeout-minutes: 2 + env: + # MetaDrive renders offscreen through panda3d's EGL pipe on llvmpipe + EGL_PLATFORM: surfaceless + run: python openpilot/tools/sim/tests/test_metadrive_bridge.py + + create_ui_report: + name: Create UI Report + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16"]') + || fromJSON('["ubuntu-24.04"]') }} + steps: + - uses: actions/checkout@v7 + - run: ./tools/op.sh setup + - name: Install raylib headless dependencies + if: ${{ !contains(runner.name, 'nsc') }} + run: sudo apt-get install -y --no-install-recommends libgles2 libegl1 + - name: Build openpilot + run: scons + - name: Create UI Report + env: + RAYLIB_BACKEND: headless + run: | + python3 openpilot/selfdrive/ui/tests/diff/replay.py + python3 openpilot/selfdrive/ui/tests/diff/replay.py --big + - name: Upload UI Report + uses: actions/upload-artifact@v7 + with: + name: ui-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + path: openpilot/selfdrive/ui/tests/diff/report diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml new file mode 100644 index 000000000..c6bc63c8a --- /dev/null +++ b/.github/workflows/ui_preview.yaml @@ -0,0 +1,176 @@ +name: "ui preview" +on: + push: + branches: + - master + pull_request_target: + types: [assigned, opened, synchronize, reopened, edited] + branches: + - 'master' + paths: + - 'openpilot/selfdrive/assets/**' + - 'openpilot/selfdrive/ui/**' + - 'openpilot/system/ui/**' + workflow_dispatch: + +env: + UI_JOB_NAME: "Create UI Report" + REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} + BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-ui-preview" + REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports" + + # variant:video_prefix:master_branch + VARIANTS: "mici:mici_ui_replay:openpilot_master_ui_mici_raylib big:tizi_ui_replay:openpilot_master_ui_big_raylib" + +jobs: + preview: + if: false # tmp disable due to GH API rate limiting flakiness + #if: github.repository == 'sunnypilot/sunnypilot' + name: preview + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + pull-requests: write + actions: read + steps: + - uses: actions/checkout@v7 + with: + submodules: true + + - name: Waiting for ui generation to end + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 + with: + ref: ${{ env.SHA }} + check-name: ${{ env.UI_JOB_NAME }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + allowed-conclusions: success + wait-interval: 20 + + - name: Getting workflow run ID + id: get_run_id + run: | + echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?[0-9]+)") | .number')" >> $GITHUB_OUTPUT + + - name: Getting proposed ui + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + run_id: ${{ steps.get_run_id.outputs.run_id }} + search_artifacts: true + name: ui-report-1-${{ env.REPORT_NAME }} + path: ${{ github.workspace }}/pr_ui + + - name: Getting mici master ui + uses: actions/checkout@v7 + with: + repository: sunnypilot/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/master_mici + ref: openpilot_master_ui_mici_raylib + + - name: Getting big master ui + uses: actions/checkout@v7 + with: + repository: sunnypilot/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/master_big + ref: openpilot_master_ui_big_raylib + + - name: Saving new master ui + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + run: | + for variant in $VARIANTS; do + IFS=':' read -r name video branch <<< "$variant" + master_dir="${{ github.workspace }}/master_${name}" + cd "$master_dir" + git checkout --orphan=new_branch + git rm -rf * + git branch -D "$branch" + git branch -m "$branch" + git config user.name "GitHub Actions Bot" + git config user.email "<>" + cp "${{ github.workspace }}/pr_ui/${video}.mp4" . + git add . + git commit -m "${name} video for commit ${{ env.SHA }}" + git push origin "$branch" --force + done + + - name: Setup FFmpeg + uses: AnimMouse/setup-ffmpeg@178e0b4a408f06ce40d896c3cd79f50fa6f8b0c3 + + - name: Finding diffs + if: github.event_name == 'pull_request_target' + id: find_diff + run: | + export PYTHONPATH=${{ github.workspace }} + baseurl="https://github.com/sunnypilot/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}" + + COMMENT="" + for variant in $VARIANTS; do + IFS=':' read -r name video _ <<< "$variant" + diff_name="${name}_diff" + + mv "${{ github.workspace }}/pr_ui/${video}.mp4" "${{ github.workspace }}/pr_ui/${video}_proposed.mp4" + cp "${{ github.workspace }}/master_${name}/${video}.mp4" "${{ github.workspace }}/pr_ui/${video}_master.mp4" + + diff_exit_code=0 + python3 ${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/diff.py \ + "${{ github.workspace }}/pr_ui/${video}_master.mp4" \ + "${{ github.workspace }}/pr_ui/${video}_proposed.mp4" \ + "${diff_name}.html" --basedir "$baseurl" --no-open || diff_exit_code=$? + + cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.html" "${{ github.workspace }}/pr_ui/" + cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.mp4" "${{ github.workspace }}/pr_ui/" + + REPORT_URL="https://sunnypilot.github.io/ci-artifacts/${diff_name}_pr_${{ github.event.number }}.html" + if [ $diff_exit_code -eq 0 ]; then + COMMENT+="**${name}**: Videos are identical! [View Diff Report]($REPORT_URL)"$'\n' + else + COMMENT+="**${name}**: ⚠️ Videos differ! [View Diff Report]($REPORT_URL)"$'\n' + fi + done + + { + echo "COMMENT<> "$GITHUB_OUTPUT" + + - name: Saving proposed ui + if: github.event_name == 'pull_request_target' + working-directory: ${{ github.workspace }}/master_mici + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + git checkout --orphan=${{ env.BRANCH_NAME }} + git rm -rf * + mv ${{ github.workspace }}/pr_ui/* . + git add . + git commit -m "ui videos for PR #${{ github.event.number }}" + git push origin ${{ env.BRANCH_NAME }} --force + + # Append diff reports to report files branch + git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }} + git checkout ${{ env.REPORT_FILES_BRANCH_NAME }} + for variant in $VARIANTS; do + IFS=':' read -r name _ _ <<< "$variant" + diff_name="${name}_diff" + cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.html" "${diff_name}_pr_${{ github.event.number }}.html" + git add "${diff_name}_pr_${{ github.event.number }}.html" + done + git commit -m "ui diff reports for PR #${{ github.event.number }}" || echo "No changes to commit" + git push origin ${{ env.REPORT_FILES_BRANCH_NAME }} + + - name: Comment on PR + if: github.event_name == 'pull_request_target' + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b + with: + message: | + + ## UI Preview + ${{ steps.find_diff.outputs.COMMENT }} + comment-tag: run_id_ui_preview + pr-number: ${{ github.event.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/wait-for-action/action.yaml b/.github/workflows/wait-for-action/action.yaml new file mode 100644 index 000000000..01bc61461 --- /dev/null +++ b/.github/workflows/wait-for-action/action.yaml @@ -0,0 +1,52 @@ +name: 'Wait for Tests' +description: 'Action to wait for workflow tests to start and complete' +inputs: + workflow: + description: 'The workflow file name to monitor' + required: true + default: 'tests.yaml' + branch: + description: 'The branch to monitor (defaults to current branch)' + required: false + default: '' + github-token: + description: 'GitHub token for API access' + required: true + wait-time: + description: 'Initial sleep time in seconds before monitoring starts' + required: false + default: '30' + should-wait-for-start: + description: 'Whether to wait for tests to start' + required: false + default: false + +runs: + using: 'composite' + steps: + - name: Wait for tests to start + if: inputs.should-wait-for-start == 'true' + shell: bash + run: | + echo "Sleeping for ${{ inputs.wait-time }} seconds to give some time for the action to start and then we'll wait" + sleep ${{ inputs.wait-time }} + + - name: Wait for tests to finish + shell: bash + run: | + BRANCH="${{ inputs.branch || github.head_ref || github.ref_name }}" + + echo "Looking for workflow runs of ${{ inputs.workflow }} on branch $BRANCH" + RUN_ID=$(gh run list --workflow=${{ inputs.workflow }} --branch="$BRANCH" --limit=1 --json databaseId --jq '.[0].databaseId') + echo "Watching run ID: $RUN_ID" + gh run watch "$RUN_ID" + + CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') + echo "Run concluded with: $CONCLUSION" + + if [[ "$CONCLUSION" != "success" ]]; then + echo "❌ Workflow run failed with conclusion: $CONCLUSION" + exit 1 + fi + env: + GITHUB_TOKEN: ${{ inputs.github-token }} diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index d423c8e14..33ea83cab 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -130,8 +130,8 @@ inline static std::unordered_map keys = { {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, - {"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, - {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"Version", {PERSISTENT, STRING}}, // --- sunnypilot params --- // @@ -195,15 +195,16 @@ inline static std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, - {"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, + {"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync? + {"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}}, {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, - {"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, + {"ModelManager_LastSyncTime_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, - {"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}}, + {"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}}, // Neural Network Lateral Control {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/openpilot/selfdrive/assets/icons_mici/egpu.png b/openpilot/selfdrive/assets/icons_mici/chestnut.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu.png rename to openpilot/selfdrive/assets/icons_mici/chestnut.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_green.png b/openpilot/selfdrive/assets/icons_mici/chestnut_green.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_green.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_green.png diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_orange.png b/openpilot/selfdrive/assets/icons_mici/chestnut_orange.png new file mode 100644 index 000000000..d3982abf8 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/chestnut_orange.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820 +size 13079 diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_crossed.png b/openpilot/selfdrive/assets/icons_mici/egpu_crossed.png deleted file mode 100644 index 82f9b0146590c0f064305ff5a773ccb5bafae693..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2034 zcmVlnNjfFsWd7E7(xMLIp`GAgO?%0-yp96@(6~tpKRtqC2t~&&!e} zIU~#WJ4YwsjO|gZk1JW$0~iPqU6^!hQbk*yOuB&}^kF1XCe?Hr{!Bp-j^U6;Boa=; zRTTMCi$uZ>O*>}@wMZoVhQg>tB4GoCQ;S3b3sg}!wMbMZe+)SGE5cDGRU*-hA=`mi zQyjHOG-Jr!kth-g{fI`DW{Lv)ln@K;L zpWi~uAXp$qBq|CP>9a{6hm=K>K#WLKBrB8deaa(RAVwrA5)CWSs_&pA0x=>{u{@aa zcz5c1{s%)|WjJXH{Tka=e+UYZuRRjwL)h}lq)*2_I1q^JcxQz` zM51C@n(}zxdjxGq9Y%NZ<x83`@=-z1Twi`6^Xp87f@*!Bb9dy~Yhq_hR zEp~4(|6Y+)XTmNQu&!*_M4+Z{>JukuNF)*tdBIVrjX*a&2P#M;5)Rq4h1v-81pBDg zhlWHVVT(3paJg_)Uk4{aBoel~;3~`&dAcY_btY_C4i$(|5QzkfVu3C&90id`0A6w% zh}!{6grQMZo9MRZkc&JO9QoxP$MrjsuTikE`*s$ z)WZ2T=?*sUuBxLrQjP~wn5jfeBho*x(P%cXKnm$lu!R>K)ut>F(?p|3%mbBlL}JUa zp^pR4*O<0M{GMoy88C)GNkaJ4B{ICh3`i}Ic1;5rLQWEqeWd2=eDJwellPA(oYVrX zk(kVStU0PIP2%+;eLDD7D4f&+eZdH1_}YWpc%RI*mX71suv21+C3zLr_hAW7XM6eh z;Bm@V6ijM?Tu)!0O6Vu*A`WB z2s8o;Gk)z+66X?#Q4onH&p8ER6v~$<)!?%%Ik!NJLfH~26if~RF$(2M^cn?|i$IJ* zVG=dhkSgpMQ7A7GX%x(#I6oSPZ>^VzcoE4GnZjp@VrF|NSc$GfXL}^dN8V6E0>7)m zNKpQZTupse2IW?TB}FDLj!)47F(T2p6fY1X5{<_!1Y$&@ahQ!jj7XFoUR=TX^yGap zL*M5ozDuKj@HuZqlyt@?<+Gr}Mc))C8n*b??|kvfVd($fbY-4_KrUA1vdO2&n&I#^*KWcxvGX~a!U z0t+WwiO4=}RWv?T(g9_jtYH`9<%o@R|qZ?<}J~>Ip+WtDCyEE1*0%eiMZOj?puZ? z`Baa>sc9awJ$U$x-+(}}V{Uw=+6b1K%9e-&%CI3JWF?YFbSad&!g?As3h8VVCQL4oNSH7pkr0hYBt#<;3DJl|LNp?g5RFJAL?aRj(TGGs zG`UK&@HwdYQeME3=d3^1yz3m{p)c>w^~!)UeC^>BsLAvb<}FSa9v5>Z5?Oqv^}L73 z(}nMab2;6y$Zj!o*8yB{LfGerDaaQ%Be_H(5hxJX=6cUT4ifoPVH(0dA$xu#Cx`0H zN1!ZjR%ZNYc{Z!5M*? z3%!BRa*s?&7>T^s_x4)erP;SVWUsH%xZj@}o*RiIdhL7Qphr=%TQH5j4@Rau9-AAU zE3ws?Z#Tu@z6`Qk3$a8B^I%hjH&J*}#7H!nKrM|(f5B$XwbkX+(6HSX`~(2-R*F QdH?_b07*qoM6N<$f*2f~umAu6 diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_gray.png b/openpilot/selfdrive/assets/icons_mici/egpu_gray.png deleted file mode 100644 index 7349f4af687485fbca94b2db50a65d778f72ebdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7279 zcmV-#9FXIQP)x`nIPD$wO>};~Nwe{G{4$(MAZRdKVPLku{)ys8EsqK)Qu{74n zwWB?qN)B6td#GMc76AMT;5~rXW1OkVm}h5alMuoYfXCEs%xJ#pPAthD>dYGXxoCDB ze6{Vyv?)1pO=Q)w*yb3(^BPAQ1pDsYyT<^I0DL)A*CCA*W82mkXH4`OgC!@UM)nY< zh+&A=(BLbz8Cn($MZ1q^oal8%4*EXL~?|uOA0&C|n`h%Ipvfihe!OTJn#`E~( zT6pR6o)b8-tu14m8j+NbwjuTw=vg3nfhy(nVhk!P;xKMc@>s_XKeg3+meth}{OTk* zr)L9v1>m^#fh@VB_mU@u3#=LWK zs9dqRQ=XXxZO7$UJ5YR<@L>DoWMV zE+OPbpb)}`c8A5Cf)Ij*5E8%wIz*$ZQfw@~7N=z8l4sEnd>!RT#|M#ZDwm=o8yAGT zH{DTZ_HYTY?t#j>a^}(6vlkJgUNH)h9=*XKZIbqE5A>rjJ9iRs1=}3}KY7ab9H`9*+6%IlPvgmRYK^6%QlzxKJUtFHZyd8m$2KQg=#&jd3Y zojZCnMdL(&c1Ch4XghXBlFwK=nf#3FGe7F{9d9~I5S*Qz9RheI#=hz< zq;?nM)u}DxXH4rMM^n>4+w1CiRUXWzRGyoH$F#X_82~)JckkYdc+*ulJ3E^K_<(it zuNTIz0G7+gI8}|Th-=8P#>y(L3ko={Dz6qxuI#FV?W#Il0{AMfhj25~vS+GU~^Dd{b)eiD3nFb>!<^`v- zoL!cuLsC~U>kesMoLmQ|PS5$At+?{)^kJTG94zia#TMGPj{qQ?ot;h1>>~i)l6OYs zJc&cyb8RP|EVy%0sh@0TB{|3j&?H(e#Il^Eu59%+5!lmR<#^aL&ospmc_uj2_G0s% z9O9Y98g1#NSqMx~U%Bsn0{C*v%+BsdPIoNx6g)b)%lN_6} zBDsvI=YSmj1eclF&NhJCq^uXt5(Uuj@T6VFiF!`r-4if(A;&lw3o?sf3H=Es#V3a3 zQR&%%aj=7VcoQL@&d%t^xHb9!G{_5lJsY|GtGao%De#_dg%Bng<~;B7C|8BNMP1F} zySJ_o9KGoQ{$g9(^QU4kr!Mo?cDZ$(;^gvNM`^U|_)15se8u4nJa3uc*)E6Av)fQ# z8i3o|W_Cvka{}FnlTkqH@%1^HgG;}mJrx&|QfFsp6KhU5PD2RanAr}R(~dc(;b;*u zH@4VMIGgV~jOz7`UMo%DbC-3?rfS(KECulY}2_)og8vtB}5Z-(1E22Ze@A3B5 zm5ec?LBEr-Ay1g9BA(}70(cQZc&l6hz-)hOh&``8FEm zYbq<%Oj7~C%Mik|v5pgIcXxLMV19af`W($yEw@CTyZ-qzsnz8@fcI@DAEMdTPN`ZB zkGBk{rN2r~FDQ7y;no(~(c^@2X2R>0+&V%RhQZ9{ySuwjuIfBYySuw_WIZp)^uqC< zN-eHu0KNslQwQ72TgYB_(8fi6vREIQk_)`N_BuG@)E4&+q5Ce3&(N%SC7q*8dxH5K zjG4`6v)OU2Hnp_7yZhYCUeb2FgCO`jd?So{9bBv3-QB+dc-G3~fz)#vPE8CDJBfMv z@iv{$Ee$Y*IJ;3FcK5uU*WIj{VGHTo6?`ryutTsQ&;pzCA003N&;JbPi8Pzdo`w+4 zBPMqAT+ZXKqS zUE+`vMotbW0RRcdZQGBt+3Z=BCfB36nOy?VQ-_^X<1coeNVD1OiJ6^iIVEhXDE2|@ z8_hn)YCw{m9uyEAK_d*{o~C5XnxwUDP7bYc_mWgyV=DI?%FHZTqvUA0T<(Mr9=B

I1JY<5v+!Yn_brMYiVxn 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 JPDHLkV1mJ!a7F+C diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png b/openpilot/selfdrive/assets/icons_mici/egpu_orange.png deleted file mode 100644 index bdad8546765704c89053a27f626bd6fbb025988b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10876 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 Nz^37&!R3wNoS5?` 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 bool: +def chestnut_present() -> bool: for d in USB_DEVICES_PATH.glob("*"): try: usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) @@ -56,5 +56,5 @@ def usbgpu_present() -> bool: pass return False -def usbgpu_compiled() -> bool: - return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file() +def chestnut_compiled() -> bool: + return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file() diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 790ae7b16..f3f639d6d 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +from collections.abc import Callable +import ctypes from functools import cached_property import os -os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom +os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device import struct @@ -30,7 +32,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase @@ -94,8 +96,10 @@ class ChestnutState: if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: smu = Device["AMD"].iface.dev_impl.smu + metrics_t = smu.smu_mod.SmuMetricsExternal_t smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) - metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics + metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:]) + metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], 'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], 'powerDrawW': metrics.AverageSocketPower, @@ -141,18 +145,18 @@ class FrameMeta: class ModelState(ModelStateBase): prev_desire: np.ndarray # for tracking the rising edge of the pulse - def __init__(self, cam_w: int, cam_h: int, usbgpu: bool): + def __init__(self, cam_w: int, cam_h: int, chestnut: bool): ModelStateBase.__init__(self) - input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) + input_devices = get_tg_input_devices(PROCESS_NAME, chestnut) self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] - jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu))) + jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut))) metadata = jits['metadata'] self.input_shapes = metadata['input_shapes'] self.vision_input_names = [k for k in self.input_shapes if 'img' in k] self.output_slices = metadata['output_slices'] self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.usbgpu = usbgpu + self.chestnut = chestnut self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) @@ -168,7 +172,7 @@ class ModelState(ModelStateBase): return parsed_model_outputs def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None: + inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]: for key in bufs.keys(): ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data yuv_size = self.frame_buf_params[key][3] @@ -192,11 +196,11 @@ class ModelState(ModelStateBase): outs, = self.run_policy( **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped ) + if after_enqueue is not None: + after_enqueue() model_output = outs.numpy()[0] - if self.usbgpu and not np.all(np.isfinite(model_output)): - # TODO remove with prev_feat - cloudlog.error("model output not finite, dropping frame") - return None + if self.chestnut and not np.all(np.isfinite(model_output)): + raise RuntimeError("model output not finite") outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] @@ -218,12 +222,12 @@ class ModelState(ModelStateBase): def main(demo=False): cloudlog.warning("modeld init") - USBGPU = usbgpu_present() and usbgpu_compiled() - if USBGPU: + CHESTNUT = chestnut_present() and chestnut_compiled() + if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() - params.put_bool("UsbGpuLoading", USBGPU) - params.remove("UsbGpuActive") + params.put_bool("ChestnutLoading", CHESTNUT) + params.remove("ChestnutActive") config_realtime_process(7, 54) @@ -253,7 +257,7 @@ def main(demo=False): st = time.monotonic() cloudlog.warning("loading model") model = None - if USBGPU: + if CHESTNUT: big_model = None def load_big(): nonlocal big_model @@ -267,23 +271,23 @@ def main(demo=False): loader.start() loader.join(BIG_MODEL_TIMEOUT) model = big_model - params.put_bool("UsbGpuActive", model is not None) + params.put_bool("ChestnutActive", model is not None) - small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None + small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None if model is None: model = small_model - params.put_bool("UsbGpuLoading", False) + params.put_bool("ChestnutLoading", False) assert model is not None cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None + chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -393,13 +397,15 @@ def main(demo=False): mt1 = time.perf_counter() try: - model_output = model.run(bufs, transforms, inputs) + send_chestnut = (chestnut_state is not None and + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) + model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: - if not params.get_bool("UsbGpuActive"): + if not params.get_bool("ChestnutActive"): raise # fallback to small model cloudlog.exception("big model failed, fall back to small") - params.put_bool("UsbGpuActive", False) + params.put_bool("ChestnutActive", False) assert small_model is not None model = small_model if chestnut_state is not None: @@ -419,7 +425,7 @@ def main(demo=False): fill_model_msg(modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen) - modelv2_send.modelV2.big = model.usbgpu + modelv2_send.modelV2.big = model.chestnut desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] @@ -441,10 +447,6 @@ def main(demo=False): pm.send('modelDataV2SP', mdv2sp_send) last_vipc_frame_id = meta_main.frame_id - if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0: - chestnut_state.send() - - if __name__ == "__main__": try: import argparse diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 1813c6703..8a869075a 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -196,17 +196,17 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.joystickDebug) self.startup_event = None - loading = self.params.get_bool("UsbGpuLoading") + loading = self.params.get_bool("ChestnutLoading") if self.big_model_loading and not loading: self.big_model_ready_t = time.monotonic() self.big_model_loading = loading if self.big_model_loading: self.events.add(EventName.bigModelLoading) - big_active = self.params.get("UsbGpuActive") - usbgpu_present = self.sm['deviceState'].chestnutPresent + big_active = self.params.get("ChestnutActive") + chestnut_present = self.sm['deviceState'].chestnutPresent model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2'] - big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present) + big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present) if big_failed and not self.big_model_failed: self.events.add(EventName.bigModelFailed) self.big_model_failed = big_failed diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index fd7497940..fce3db460 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.common.version import RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 @@ -139,8 +139,8 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) - self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37)) - self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37)) + self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) + self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -150,8 +150,8 @@ class MiciHomeLayout(Widget): IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), NetworkIcon(), self._experimental_icon, - self._egpu_icon, - self._egpu_icon_gray, + self._chestnut_icon, + self._chestnut_failed_icon, self._body_icon, self._mic_icon, ], spacing=18) @@ -249,10 +249,10 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) if gui_app.sunnypilot_ui(): - self._set_egpu_visibility() + self._set_chestnut_visibility() else: - self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled) - self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled) + self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) + self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index 2d1f95412..05abae82e 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.common.constants import CV from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -107,8 +107,7 @@ class HudRenderer(Widget): self.speed: float = 0.0 self.v_ego_cluster_seen: bool = False self._engaged: bool = False - self._small_model_engaged: bool = False - self._egpu_fade_time: float = 0 + self._chestnut_fade_time: float = 0 self._can_draw_top_icons = True self._show_wheel_critical = False @@ -124,17 +123,15 @@ class HudRenderer(Widget): self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) - self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44) - self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44) - self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44) - self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52) - self._egpu_icon: rl.Texture | None = None - + self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) + self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) + self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) + self._chestnut_icon: rl.Texture | None = None self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - self._egpu_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) def set_wheel_critical_icon(self, critical: bool): """Set the wheel icon to critical or normal state.""" @@ -165,13 +162,10 @@ class HudRenderer(Widget): controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster ) engaged = sm['selfdriveState'].enabled - if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and - ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): - self._small_model_engaged = True - if engaged != self._engaged: - self._egpu_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() + if engaged != self._engaged: + self._chestnut_fade_time = rl.get_time() if engaged else 0 self._engaged = engaged self.set_speed = set_speed self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA @@ -191,8 +185,7 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) - if ui_state.usbgpu and ui_state.usbgpu_compiled: - self._draw_model_source(rect) + self._draw_model_source(rect) self._draw_steering_wheel(rect) @@ -200,30 +193,24 @@ class HudRenderer(Widget): if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: return - big_failed = (ui_state.usbgpu_active is False or not ui_state.sm['deviceState'].chestnutPresent or - (ui_state.usbgpu_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and - not ui_state.sm.alive['modelV2']) or - (ui_state.usbgpu_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) - self._small_model_engaged &= big_failed - loading = ui_state.usbgpu_loading or (ui_state.usbgpu_active is None and not big_failed) + loading = ui_state.chestnut_state == ChestnutState.LOADING if loading: - pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) - icon = self._txt_egpu - opacity = 0.35 + 0.65 * pulse - elif self._small_model_engaged: - icon = self._txt_egpu_crossed - opacity = 0.65 - elif big_failed: - icon = self._txt_egpu_orange + icon = self._txt_chestnut + opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): + icon = self._txt_chestnut_orange + opacity = 1.0 + elif ui_state.chestnut_state == ChestnutState.ACTIVE: + icon = self._txt_chestnut_green opacity = 1.0 else: - icon = self._txt_egpu_green - opacity = 1.0 + return - if icon is not self._egpu_icon: - self._egpu_fade_time = rl.get_time() - self._egpu_icon = icon - alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE) + if icon is not self._chestnut_icon: + self._chestnut_fade_time = rl.get_time() + self._chestnut_icon = icon + visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE + alpha = self._chestnut_alpha_filter.update(visible) if alpha < 1e-2: return diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index f012cd6d3..678682aea 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -62,14 +62,14 @@ class ModelsLayout(Widget): self.big_model_item = ListItemSP( title=tr("Big Model"), action_item=ScrollingButtonAction(tr("SELECT")), - callback=lambda: self._open_source_dialog("usbgpu") + callback=lambda: self._open_source_dialog("chestnut") ) self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), - ui_state.params.put("ModelManager_LastSyncTime_USBGPU", 0), + ui_state.params.put("ModelManager_LastSyncTime_Chestnut", 0), gui_app.push_widget(alert_dialog(tr("Fetching Latest Models"))))) self.clear_cache_item = ListItemSP( @@ -177,14 +177,14 @@ class ModelsLayout(Widget): big_state = big_model_state() carry_source, carry_internal, _ = carrying_model() segments = [] - for source, label in (("qcom", tr("small")), ("usbgpu", tr("big"))): + for source, label in (("qcom", tr("small")), ("chestnut", tr("big"))): if segments: segments.append(("|", rl.GRAY, None, None)) bundle = get_selected_bundle(ui_state.params, source) name = bundle.internalName if bundle else default_model_name(source) color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY name = "● " + name - if source == "usbgpu": + if source == "chestnut": if big_state == 'failed': color = rl.RED elif big_state == 'loading': @@ -208,10 +208,10 @@ class ModelsLayout(Widget): """The failover story for the Model Status row. One-way big -> small, and the fallback is runner-matched: a Default big can only fall back to the Default small (stock modeld), a custom big has no automatic fallback yet.""" - if not ui_state.usbgpu: + if not ui_state.chestnut_present: return "" - big_bundle = get_selected_bundle(ui_state.params, "usbgpu") - big_name = big_bundle.internalName if big_bundle else default_model_name("usbgpu") + big_bundle = get_selected_bundle(ui_state.params, "chestnut") + big_name = big_bundle.internalName if big_bundle else default_model_name("chestnut") big_is_default = big_bundle is None fallback_name = default_model_name("qcom") state = big_model_state() @@ -225,7 +225,7 @@ class ModelsLayout(Widget): return tr("Getting the big model ready.") if big_is_default: return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name) - return tr("{} will drive when the eGPU is ready.").format(big_name) + return tr("{} will drive when the chestnut is ready.").format(big_name) @staticmethod def _download_row_state(progresses, name: str) -> dict: @@ -261,7 +261,7 @@ class ModelsLayout(Widget): ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref) def _resolve_selected_bundle(self, ref): - source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")} + source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")} resolved = resolve_bundle_by_ref(ref, source_bundles) return resolved[0] if resolved else None @@ -329,7 +329,7 @@ class ModelsLayout(Widget): self._handle_bundle_download_progress() carry_source, _, carry_display = carrying_model() - for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "usbgpu")): + for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "chestnut")): bundle = get_selected_bundle(ui_state.params, item_source) name = bundle.displayName if bundle else default_model_name(item_source) color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py index 0f7d4c67d..a0b627ad0 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py @@ -9,7 +9,7 @@ import math import pyray as rl import time from dataclasses import dataclass -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr_noop @@ -21,8 +21,8 @@ METRIC_MARGIN = 30 METRIC_START_Y = 300 HOME_BTN = rl.Rectangle(60, 860, 180, 180) -EGPU_ICON_WIDTH = 180 -EGPU_ICON_HEIGHT = 133 +CHESTNUT_ICON_WIDTH = 180 +CHESTNUT_ICON_HEIGHT = 133 # Color scheme @@ -59,10 +59,9 @@ class MetricData: class SidebarSP: def __init__(self): self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING) - self._egpu_green_img = gui_app.texture("icons_mici/egpu_green.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) - self._egpu_default_img = gui_app.texture("icons_mici/egpu.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) - self._egpu_orange_img = gui_app.texture("icons_mici/egpu_orange.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) - self._egpu_gray_img = gui_app.texture("icons_mici/egpu_gray.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) + self._chestnut_green_img = gui_app.texture("icons_mici/chestnut_green.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT) + self._chestnut_default_img = gui_app.texture("icons_mici/chestnut.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT) + self._chestnut_orange_img = gui_app.texture("icons_mici/chestnut_orange.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT) def _update_sunnylink_status(self): if not ui_state.params.get_bool("SunnylinkEnabled"): @@ -90,22 +89,17 @@ class SidebarSP: def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]: default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y) - if not ui_state.sm["deviceState"].chestnutPresent: + state = ui_state.chestnut_state + if state == ChestnutState.DISCONNECTED: return default_img, default_pos, 1.0 - big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad - big_model_failed = ui_state.started and ui_state.big_model_failed - loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) - - if loading: - icon = self._egpu_default_img + if state == ChestnutState.LOADING: + icon = self._chestnut_default_img opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) - elif big_model_selected and big_model_failed: - icon, opacity = self._egpu_orange_img, 1.0 - elif big_model_selected: - icon, opacity = self._egpu_green_img, 1.0 + elif state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): + icon, opacity = self._chestnut_orange_img, 1.0 else: - icon, opacity = self._egpu_gray_img, 1.0 + icon, opacity = self._chestnut_green_img, 1.0 x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2 y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2 diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py index b26137394..8efb6c80e 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py @@ -9,7 +9,7 @@ import math import pyray as rl from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel @@ -19,35 +19,16 @@ class MiciHomeLayoutSP(MiciHomeLayout): def __init__(self): super().__init__() self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) - self._egpu_icon_default = IconWidget("icons_mici/egpu.png", (50, 37)) - self._egpu_icon_default.set_visible(False) - self._egpu_icon_orange = IconWidget("icons_mici/egpu_orange.png", (50, 37)) - self._egpu_icon_orange.set_visible(False) - gray_idx = self._status_bar_layout.widgets.index(self._egpu_icon_gray) - self._status_bar_layout.widgets.insert(gray_idx + 1, self._egpu_icon_default) - self._status_bar_layout.widgets.insert(gray_idx + 2, self._egpu_icon_orange) + self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40)) + self._chestnut_loading_icon.set_visible(False) + failed_idx = self._status_bar_layout.widgets.index(self._chestnut_failed_icon) + self._status_bar_layout.widgets.insert(failed_idx + 1, self._chestnut_loading_icon) - def _set_egpu_visibility(self): - chestnut = ui_state.sm["deviceState"].chestnutPresent - if not chestnut: - self._egpu_icon.set_visible(False) - self._egpu_icon_default.set_visible(False) - self._egpu_icon_orange.set_visible(False) - self._egpu_icon_gray.set_visible(False) - return - - big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad - big_model_failed = ui_state.started and ui_state.big_model_failed - loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) - - if loading: - self._egpu_icon_default._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) - self._egpu_icon_default.set_visible(True) - self._egpu_icon.set_visible(False) - self._egpu_icon_orange.set_visible(False) - self._egpu_icon_gray.set_visible(False) - else: - self._egpu_icon_default.set_visible(False) - self._egpu_icon.set_visible(big_model_selected and not big_model_failed) - self._egpu_icon_orange.set_visible(big_model_selected and big_model_failed) - self._egpu_icon_gray.set_visible(not big_model_selected) + def _set_chestnut_visibility(self): + # stock has no loading tier: it shows green from the moment a big model is available. keep the + # pulse so the status bar and the onroad HUD agree on what loading looks like. + loading = ui_state.chestnut_state == ChestnutState.LOADING + self._chestnut_loading_icon._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + self._chestnut_loading_icon.set_visible(loading) + self._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) + self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py index 183b47fa5..4c769498f 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -27,14 +27,14 @@ def _model_info() -> tuple[str, str, str]: state = big_model_state() _, _, carry_display = carrying_model() if carry_display is None: - big = get_selected_bundle(ui_state.params, "usbgpu") - carry_display = big.displayName if big else default_model_name("usbgpu") + big = get_selected_bundle(ui_state.params, "chestnut") + carry_display = big.displayName if big else default_model_name("chestnut") active_text = (carry_display or active_name).lower() if state == 'failed': return active_text, tr("big model"), tr("unavailable") if state == 'loading': return active_text, tr("big model"), tr("getting ready") - header = tr("small model") if source == "usbgpu" else tr("big model") + header = tr("small model") if source == "chestnut" else tr("big model") return active_text, header, other_name.lower() @@ -113,7 +113,7 @@ class ModelsLayoutMici(NavScroller): hardware_btns = [] active = active_source() - for source, label in (("qcom", tr("small models")), ("usbgpu", tr("big models"))): + for source, label in (("qcom", tr("small models")), ("chestnut", tr("big models"))): bundle = get_selected_bundle(ui_state.params, source) value = (bundle.internalName if bundle else default_model_name(source)).lower() if source == active: diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py index ad75f7e96..9d39d0172 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py @@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details. import pyray as rl from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer -from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators @@ -22,8 +21,6 @@ class HudRendererSP(HudRenderer): def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) - if ui_state.usbgpu and not ui_state.usbgpu_compiled and ui_state.model_runner_tinygrad: - self._draw_model_source(rect) self.blind_spot_indicators.render(rect) def _has_blind_spot_detected(self) -> bool: diff --git a/openpilot/selfdrive/ui/sunnypilot/model_info.py b/openpilot/selfdrive/ui/sunnypilot/model_info.py index a93a06f18..e338feb9e 100644 --- a/openpilot/selfdrive/ui/sunnypilot/model_info.py +++ b/openpilot/selfdrive/ui/sunnypilot/model_info.py @@ -4,15 +4,15 @@ 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. """ -from openpilot.selfdrive.ui.ui_state import ui_state +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 active_source() -> str: - return get_active_source(usbgpu=ui_state.usbgpu, - usbgpu_active=ui_state.usbgpu_active, usbgpu_loading=ui_state.usbgpu_loading, + return get_active_source(chestnut=ui_state.chestnut_present, + chestnut_active=ui_state.chestnut_active, chestnut_loading=ui_state.chestnut_loading, offroad=ui_state.is_offroad()) @@ -23,7 +23,7 @@ def bundles_for_source(source: str): def default_model(source: str) -> str: - return DEFAULT_BIG_MODEL if source == 'usbgpu' else DEFAULT_MODEL + return DEFAULT_BIG_MODEL if source == 'chestnut' else DEFAULT_MODEL def default_model_name(source: str) -> str: @@ -31,13 +31,10 @@ def default_model_name(source: str) -> str: def big_model_state() -> str | None: - """'failed' | 'loading' | None, mirroring the sidebar's detection (#1969).""" - if ui_state.started and ui_state.usbgpu and ui_state.big_model_failed: - return 'failed' - big_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad - if ui_state.usbgpu_loading or (big_selected and ui_state.started and ui_state.usbgpu_active is None): - return 'loading' - return None + """'failed' | 'loading' | None, from the same state the icons render.""" + return {ChestnutState.UNCOMPILED: 'failed', + ChestnutState.FAILED: 'failed', + ChestnutState.LOADING: 'loading'}.get(ui_state.chestnut_state) def carrying_model() -> tuple[str | None, str | None, str | None]: @@ -45,14 +42,14 @@ def carrying_model() -> tuple[str | None, str | None, str | None]: when a Default big cannot carry, stock modeld runs the Default small, never the small slot's pick; a custom big has no automatic fallback yet -> (None, None, None).""" source = active_source() - if source == "usbgpu": - bundle = get_selected_bundle(ui_state.params, "usbgpu") + if source == "chestnut": + bundle = get_selected_bundle(ui_state.params, "chestnut") if bundle: - return "usbgpu", bundle.internalName, bundle.displayName - name = default_model_name("usbgpu") - return "usbgpu", name, name - if ui_state.usbgpu: - if get_selected_bundle(ui_state.params, "usbgpu") is None: + return "chestnut", bundle.internalName, bundle.displayName + name = default_model_name("chestnut") + return "chestnut", name, name + if ui_state.chestnut_present: + if get_selected_bundle(ui_state.params, "chestnut") is None: name = default_model_name("qcom") return "qcom", name, name return None, None, None @@ -66,7 +63,7 @@ def carrying_model() -> tuple[str | None, str | None, str | None]: def queued_name(current_ref) -> str | None: ref = ui_state.params.get("ModelManager_DownloadRef") if ref and ref != current_ref: - source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")} + source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")} if resolved := resolve_bundle_by_ref(ref, source_bundles): return resolved[0].internalName return None @@ -79,7 +76,7 @@ def model_info() -> tuple[str, str, str]: manager republishes a tick after a chestnut change, so the stale bundle would flash the wrong model.""" source = active_source() - other = "qcom" if source == "usbgpu" else "usbgpu" + other = "qcom" if source == "chestnut" else "chestnut" active_bundle = get_selected_bundle(ui_state.params, source) other_bundle = get_selected_bundle(ui_state.params, other) diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index 9bed533d3..d130c3862 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -152,10 +152,13 @@ class UIStateSP: self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self._enforce_constraints() - source = get_active_source(usbgpu=self.usbgpu, usbgpu_active=self.usbgpu_active, - usbgpu_loading=self.usbgpu_loading, offroad=self.is_offroad()) + source = get_active_source(chestnut=self.chestnut_present, chestnut_active=self.chestnut_active, + chestnut_loading=self.chestnut_loading, offroad=self.is_offroad()) self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source]) self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad" + # stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the + # chestnut just the same, so ChestnutState has to see it as available too. + self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad self.blindspot = self.params.get_bool("BlindSpot") self.chevron_metrics = self.params.get("ChevronInfo") self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 7e2a2494e..4d93e1aac 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,7 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC -from openpilot.selfdrive.modeld.helpers import usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import chestnut_compiled from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP @@ -28,6 +28,15 @@ class UIStatus(Enum): LONG_ONLY = "long_only" +class ChestnutState(Enum): + DISCONNECTED = "disconnected" + UNCOMPILED = "uncompiled" + READY = "ready" + LOADING = "loading" + ACTIVE = "active" + FAILED = "failed" + + class UIState(UIStateSP): _instance: 'UIState | None' = None @@ -82,10 +91,11 @@ class UIState(UIStateSP): self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") - self.usbgpu: bool = False - self.usbgpu_compiled: bool = usbgpu_compiled() - self.usbgpu_active: bool | None = self.params.get("UsbGpuActive") - self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading") + self.chestnut_present: bool = False + self.chestnut_compiled: bool = chestnut_compiled() + self.chestnut_active: bool | None = None + self.chestnut_loading: bool = False + self.chestnut_state = ChestnutState.DISCONNECTED self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -112,15 +122,6 @@ class UIState(UIStateSP): def add_on_body_changed_callbacks(self, callback: Callable[[], None]): self._on_body_changed_callbacks.append(callback) - @property - def big_model_failed(self) -> bool: - # Mirrors the onroad HUD's four-condition check so sidebar and home icons reflect the same failure states - return (self.usbgpu_active is False or - not self.sm['deviceState'].chestnutPresent or - (self.usbgpu_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and - not self.sm.alive['modelV2']) or - (self.usbgpu_active is None and self.sm.recv_frame['modelV2'] > self.started_frame)) - @property def engaged(self) -> bool: return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled) @@ -140,6 +141,7 @@ class UIState(UIStateSP): self.sm.update(0) self._update_state() self._update_status() + self._update_chestnut_state() device.update() UIStateSP.update(self) @@ -203,12 +205,35 @@ class UIState(UIStateSP): self.status = UIStatus.DISENGAGED self.started_frame = self.sm.frame self.started_time = time.monotonic() + self.chestnut_present = self.sm["deviceState"].chestnutPresent for callback in self._offroad_transition_callbacks: callback() self._started_prev = self.started + def _update_chestnut_state(self) -> None: + detected = self.sm["deviceState"].chestnutPresent + if not self.started: + self.chestnut_present = detected + self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else + ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED) + return + + model_seen = self.sm.recv_frame["modelV2"] > self.started_frame + if not self.chestnut_present: + self.chestnut_state = ChestnutState.DISCONNECTED + elif not self.chestnut_compiled: + self.chestnut_state = ChestnutState.UNCOMPILED + elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)): + self.chestnut_state = ChestnutState.FAILED + elif self.chestnut_loading or not model_seen: + self.chestnut_state = ChestnutState.LOADING + elif self.chestnut_active is False: + self.chestnut_state = ChestnutState.FAILED + else: + self.chestnut_state = ChestnutState.ACTIVE + def update_params(self) -> None: # For slower operations # Update longitudinal control state @@ -225,12 +250,10 @@ class UIState(UIStateSP): self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - # keep usbgpu UI active until offroad transition when gpu disappears - self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started) - if not self.usbgpu_compiled: - self.usbgpu_compiled = usbgpu_compiled() - self.usbgpu_active = self.params.get("UsbGpuActive") - self.usbgpu_loading = self.params.get_bool("UsbGpuLoading") + if not self.chestnut_compiled: + self.chestnut_compiled = chestnut_compiled() + self.chestnut_active = self.params.get("ChestnutActive") + self.chestnut_loading = self.params.get_bool("ChestnutLoading") UIStateSP.update_params(self) diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index 17687908c..7a58352c3 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -298,7 +298,7 @@ def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: if __name__ == "__main__": - if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'): + if 'USB' in os.getenv('DEV', '') or os.getenv('CHESTNUT'): from openpilot.system.hardware.chestnut.flash import link_up for _ in range(10): if link_up(): diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index d18001227..d9c04d782 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -9,7 +9,7 @@ See the LICENSE.md file in the root directory for more details. import os os.environ['GMMU'] = '0' from openpilot.common.hardware import COMMA_HARDWARE -from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob import time import numpy as np import openpilot.cereal.messaging as messaging @@ -84,14 +84,14 @@ class ModelState(ModelStateBase): inputs: dict[str, np.ndarray] prev_desire: np.ndarray - def __init__(self, cam_w: int, cam_h: int, usbgpu: bool = False): + def __init__(self, cam_w: int, cam_h: int, chestnut: bool = False): ModelStateBase.__init__(self) env_pkl = os.environ.get('COMBINED_MODEL_PKL') if env_pkl and os.path.exists(env_pkl): model_bundle = None else: - model_bundle = get_active_bundle(usbgpu=usbgpu) + model_bundle = get_active_bundle(chestnut=chestnut) self.generation = model_bundle.generation if model_bundle is not None else None overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {} @@ -99,7 +99,7 @@ class ModelState(ModelStateBase): self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.MIN_LAT_CONTROL_SPEED = 0.3 self.PLANPLUS_CONTROL: float = 1.0 - self.usbgpu = usbgpu + self.chestnut = chestnut pkl_path = _find_driving_pkl(model_bundle) assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py" @@ -110,7 +110,7 @@ class ModelState(ModelStateBase): jits = load_oob(open_file_chunked(pkl_path)) self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU' - self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV + self.DEV = 'AMD' if self.chestnut else self.WARP_DEV self.QUEUE_DEV = self.DEV metadata = jits['metadata'] @@ -185,7 +185,7 @@ class ModelState(ModelStateBase): else: self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) - if self.usbgpu: + if self.chestnut: self.warmup() def warmup(self) -> None: @@ -287,7 +287,7 @@ class ModelState(ModelStateBase): buf[0, :-1] = buf[0, 1:] buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 - if self.usbgpu and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): + if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): cloudlog.error("model output not finite, dropping frame") return None @@ -327,13 +327,13 @@ def main(demo=False): setproctitle(PROCESS_NAME) config_realtime_process(7, 54) - USBGPU = usbgpu_present() - if USBGPU: + CHESTNUT = chestnut_present() + if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() - params.put_bool("UsbGpuLoading", USBGPU) - params.remove("UsbGpuActive") + params.put_bool("ChestnutLoading", CHESTNUT) + params.remove("ChestnutActive") # visionipc clients while True: @@ -362,31 +362,31 @@ def main(demo=False): st = time.monotonic() model = None - if USBGPU: + if CHESTNUT: import threading def load(): nonlocal model - model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True) + model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True) t = threading.Thread(target=load, daemon=True) t.start() t.join(60) if model is None: - params.put_bool("UsbGpuActive", False) - raise RuntimeError("eGPU model load failed or timed out (60s)") - params.put_bool("UsbGpuActive", True) + params.put_bool("ChestnutActive", False) + raise RuntimeError("chestnut model load failed or timed out (60s)") + params.put_bool("ChestnutActive", True) else: - model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False) + model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) - params.put_bool("UsbGpuLoading", False) + params.put_bool("ChestnutLoading", False) cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() - chestnut_state = ChestnutState(pm, USBGPU) if USBGPU else None + chestnut_state = ChestnutState(pm, CHESTNUT) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) @@ -524,7 +524,7 @@ def main(demo=False): fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants) - modelv2_send.modelV2.big = model.usbgpu + modelv2_send.modelV2.big = model.chestnut desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/openpilot/sunnypilot/modeld_v2/tests/helpers.py b/openpilot/sunnypilot/modeld_v2/tests/helpers.py index 6e66bf771..82e159a30 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/helpers.py +++ b/openpilot/sunnypilot/modeld_v2/tests/helpers.py @@ -190,8 +190,8 @@ def tmp_path(): def patch_modeld(monkeypatch): def _patch(bundle): - monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) - monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) return _patch diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py index ccd8cbc7f..7f40c19eb 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py @@ -59,8 +59,8 @@ class TestFindDrivingPkl(OpenpilotTestCase): class TestModelStateCombinedInit(OpenpilotTestCase): def test_asserts_when_no_pkl(self, monkeypatch): bundle = DummyBundle(models=[], is_20hz=True) - monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) - monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) with self.assertRaisesRegex(AssertionError, "No driving pkl found"): ModelState(cam_w=CAM_W, cam_h=CAM_H) diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index a8e55f8e6..69f08c2cd 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -9,8 +9,8 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MO def get_default_model() -> str: - show_big_model = (ui_state.usbgpu - and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad())) + show_big_model = (ui_state.chestnut_present + and (ui_state.chestnut_active or ui_state.chestnut_loading or ui_state.is_offroad())) return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index 1bbfb02f7..d0115be04 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -139,11 +139,11 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json" - MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json" + MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json" MODEL_SOURCES = { "qcom": (MODEL_URL, ""), - "usbgpu": (MODEL_URL_USBGPU, "_USBGPU"), + "chestnut": (MODEL_URL_CHESTNUT, "_Chestnut"), } def __init__(self, params: Params): @@ -156,12 +156,12 @@ class ModelFetcher: self._refetched: set[str] = set() self.params.put("ModelManager_ActiveJson", { "qcom": self.MODEL_URL, - "usbgpu": self.MODEL_URL_USBGPU, + "chestnut": self.MODEL_URL_CHESTNUT, }, block=True) @staticmethod def active_source(chestnut_present: bool) -> str: - return "usbgpu" if chestnut_present else "qcom" + return "chestnut" if chestnut_present else "qcom" def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None: """Fetches fresh model data from remote and updates cache. @@ -200,7 +200,7 @@ class ModelFetcher: @staticmethod def _cache_matches_source(source: str, cached_data: dict) -> bool: bundles = cached_data.get("bundles", []) - if source == "usbgpu": + if source == "chestnut": return any(bundle.get("is_big") is True for bundle in bundles) return not any(bundle.get("is_big") is True for bundle in bundles) @@ -261,10 +261,10 @@ def get_cached_bundles(params: Params, source: str) -> list[custom.ModelManagerS if __name__ == "__main__": - from openpilot.selfdrive.modeld.helpers import usbgpu_present + from openpilot.selfdrive.modeld.helpers import chestnut_present params = Params() model_fetcher = ModelFetcher(params) - bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(usbgpu_present())) + bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(chestnut_present())) for bundle in bundles: for model in bundle.models: model_overrides = {override.key: override.value for override in bundle.overrides} diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index 707b86f72..3c3cc1107 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -16,7 +16,7 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider from openpilot.common.hardware.hw import Paths -from openpilot.selfdrive.modeld.helpers import usbgpu_present +from openpilot.selfdrive.modeld.helpers import chestnut_present # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO REQUIRED_JSON_VERSION = 18 @@ -27,7 +27,7 @@ ModelManager = custom.ModelManagerSP ACTIVE_BUNDLE_KEYS = { "qcom": "ModelManager_ActiveBundle", - "usbgpu": "ModelManager_ActiveBundleUSBGPU", + "chestnut": "ModelManager_ActiveBundleChestnut", } _LAST_VALIDATED_RAW: dict[str, dict | None] = {} @@ -126,20 +126,20 @@ def get_selected_bundle(params: Params | None = None, source: str = "qcom") -> " return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source])) -def get_active_source(usbgpu: bool | None = None, usbgpu_active: bool | None = None, - usbgpu_loading: bool | None = None, offroad: bool | None = None) -> str: - if usbgpu is None: - usbgpu = usbgpu_present() - state_valid = usbgpu_active is not None or usbgpu_loading is not None or offroad is not None - big_active = usbgpu and (not state_valid or usbgpu_active or usbgpu_loading or offroad) - return "usbgpu" if big_active else "qcom" +def get_active_source(chestnut: bool | None = None, chestnut_active: bool | None = None, + chestnut_loading: bool | None = None, offroad: bool | None = None) -> str: + if chestnut is None: + chestnut = chestnut_present() + state_valid = chestnut_active is not None or chestnut_loading is not None or offroad is not None + big_active = chestnut and (not state_valid or chestnut_active or chestnut_loading or offroad) + return "chestnut" if big_active else "qcom" -def get_active_bundle(params: Params | None = None, *, usbgpu: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None": +def get_active_bundle(params: Params | None = None, *, chestnut: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None": # no cross-slot fallback: an empty active slot means the hardware default, which # only stock modeld can run - modeld_v2 requires a real bundle params = params or Params() - return get_selected_bundle(params, get_active_source(usbgpu=usbgpu)) + return get_selected_bundle(params, get_active_source(chestnut=chestnut)) def resolve_bundle_by_ref( diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 178d6c04e..16253db0f 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -40,7 +40,7 @@ class ModelManagerSP: self.available_models: list[custom.ModelManagerSP.ModelBundle] = [] self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {} self.selected_bundle: custom.ModelManagerSP.ModelBundle = None - self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) + self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present) self._chunk_size = 128 * 1000 # 128 KB chunks self._download_start_times: dict[str, float] = {} # Track start time per model self._download_ref: bytes | str | None = None @@ -280,7 +280,7 @@ class ModelManagerSP: raise DownloadCancelled("Download cancelled") self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True) - self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) + self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present) except Exception: if self.selected_bundle is not None: @@ -326,7 +326,7 @@ class ModelManagerSP: self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES} self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)] validate_active_bundles(self.params, self.source_models) - self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) + self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present) self._process_download_requests() diff --git a/openpilot/sunnypilot/models/tests/test_manager_download.py b/openpilot/sunnypilot/models/tests/test_manager_download.py index 4d3b7989f..489e0bc09 100644 --- a/openpilot/sunnypilot/models/tests/test_manager_download.py +++ b/openpilot/sunnypilot/models/tests/test_manager_download.py @@ -392,7 +392,7 @@ class TestManagerDownload(ManagerDownloadTestBase): asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom")) assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot" - assert "ModelManager_ActiveBundleUSBGPU" not in store, "qcom download must not touch the usbgpu slot" + assert "ModelManager_ActiveBundleChestnut" not in store, "qcom download must not touch the chestnut slot" assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref" assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded @@ -401,18 +401,18 @@ class TestManagerDownload(ManagerDownloadTestBase): assert missing == [], f"chunks missing from the cache: {missing}" self.run_with_server(body) - def test_download_writes_usbgpu_slot(self): - """A download resolved to the usbgpu source writes the usbgpu active bundle slot only.""" + def test_download_writes_chestnut_slot(self): + """A download resolved to the chestnut source writes the chestnut active bundle slot only.""" def body(): self.make_artifact(chunked=True) self._bundle.ref = "big-ref" self._bundle.minimumSelectorVersion = 18 params, store = self._make_params_with_store() self.manager.params = params - asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "usbgpu")) + asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut")) - assert "ModelManager_ActiveBundleUSBGPU" in store, "usbgpu download must write the usbgpu slot" - assert "ModelManager_ActiveBundle" not in store, "usbgpu download must not touch the qcom slot" + assert "ModelManager_ActiveBundleChestnut" in store, "chestnut download must write the chestnut slot" + assert "ModelManager_ActiveBundle" not in store, "chestnut download must not touch the qcom slot" assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded self.run_with_server(body) @@ -447,20 +447,20 @@ class TestResolveBundleByRef(OpenpilotTestCase): def test_qcom_ref_resolves_to_qcom_slot(self): small = self._bundle("small") - assert resolve_bundle_by_ref("small", {"qcom": [small], "usbgpu": []}) == (small, "qcom") + assert resolve_bundle_by_ref("small", {"qcom": [small], "chestnut": []}) == (small, "qcom") - def test_usbgpu_ref_resolves_to_usbgpu_slot(self): + def test_chestnut_ref_resolves_to_chestnut_slot(self): big = self._bundle("big") - assert resolve_bundle_by_ref("big", {"qcom": [], "usbgpu": [big]}) == (big, "usbgpu") + assert resolve_bundle_by_ref("big", {"qcom": [], "chestnut": [big]}) == (big, "chestnut") def test_unknown_ref_returns_none(self): - source_bundles = {"qcom": [self._bundle("small")], "usbgpu": []} + source_bundles = {"qcom": [self._bundle("small")], "chestnut": []} assert resolve_bundle_by_ref("nope", source_bundles) is None def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict: """Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects). - Big (usbgpu) bundles carry `is_big: true` in the manifest JSON.""" + Big (chestnut) bundles carry `is_big: true` in the manifest JSON.""" return { "index": index, "short_name": short_name, @@ -489,15 +489,15 @@ class TestModelFetcherSources(OpenpilotTestCase): """Both manifests are always maintained: get_bundles_for_source exposes either source by name, and active_source picks which one matches the attached hardware.""" - def _make_params(self, qcom_manifest, usbgpu_manifest): + def _make_params(self, qcom_manifest, chestnut_manifest): params = mock.MagicMock() def get(key): if key == "ModelManager_ModelsCache": return qcom_manifest - if key == "ModelManager_ModelsCache_USBGPU": - return usbgpu_manifest - if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"): + if key == "ModelManager_ModelsCache_Chestnut": + return chestnut_manifest + if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"): return fresh_sync_time() return None @@ -506,14 +506,14 @@ class TestModelFetcherSources(OpenpilotTestCase): def test_active_source_follows_chestnut_presence(self): assert ModelFetcher.active_source(False) == "qcom" - assert ModelFetcher.active_source(True) == "usbgpu" + assert ModelFetcher.active_source(True) == "chestnut" def test_get_bundles_for_source_returns_each_source(self): params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) fetcher = ModelFetcher(params) assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] - assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"] + assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"] def test_get_bundles_for_source_unknown(self): assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == [] @@ -522,16 +522,16 @@ class TestModelFetcherSources(OpenpilotTestCase): params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) qcom_bundles = get_cached_bundles(params, "qcom") - usbgpu_bundles = get_cached_bundles(params, "usbgpu") + chestnut_bundles = get_cached_bundles(params, "chestnut") assert [b.ref for b in qcom_bundles] == ["aaa"] - assert [b.ref for b in usbgpu_bundles] == ["bbb"] + assert [b.ref for b in chestnut_bundles] == ["bbb"] assert qcom_bundles[0].displayName == "SMALL" def test_get_cached_bundles_empty_when_missing(self): params = mock.MagicMock() params.get.return_value = None assert get_cached_bundles(params, "qcom") == [] - assert get_cached_bundles(params, "usbgpu") == [] + assert get_cached_bundles(params, "chestnut") == [] def test_get_cached_bundles_unknown_source(self): assert get_cached_bundles(mock.MagicMock(), "bogus") == [] @@ -543,27 +543,27 @@ class TestModelFetcherSources(OpenpilotTestCase): assert active_json_calls, "expected ModelManager_ActiveJson to be written" assert active_json_calls[-1].args[1] == { "qcom": ModelFetcher.MODEL_URL, - "usbgpu": ModelFetcher.MODEL_URL_USBGPU, + "chestnut": ModelFetcher.MODEL_URL_CHESTNUT, } class TestSourceCacheIntegrity(OpenpilotTestCase): """Each source's cached manifest must contain only that source's models; the - `is_big` flag in the JSON marks the big (usbgpu) models. A mismatched cache is + `is_big` flag in the JSON marks the big (chestnut) models. A mismatched cache is legacy data from before the per-source split (the active manifest was cached under the unsuffixed key regardless of hardware) and is refetched. This replaces the old one-time bundle migration.""" - def _make_params(self, qcom_manifest, usbgpu_manifest): + def _make_params(self, qcom_manifest, chestnut_manifest): params = mock.MagicMock() def get(key): if key == "ModelManager_ModelsCache": return qcom_manifest - if key == "ModelManager_ModelsCache_USBGPU": - return usbgpu_manifest - if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"): + if key == "ModelManager_ModelsCache_Chestnut": + return chestnut_manifest + if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"): return fresh_sync_time() return None @@ -584,13 +584,13 @@ class TestSourceCacheIntegrity(OpenpilotTestCase): bundles = fetcher.get_bundles_for_source("qcom") assert [bundle.ref for bundle in bundles] == ["aaa"] - def test_usbgpu_cache_without_big_models_is_refetched(self): + def test_chestnut_cache_without_big_models_is_refetched(self): params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, {"bundles": [manifest_bundle("big2", "ccc")]}) fetcher = ModelFetcher(params) fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True)) with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched): - bundles = fetcher.get_bundles_for_source("usbgpu") + bundles = fetcher.get_bundles_for_source("chestnut") assert [bundle.ref for bundle in bundles] == ["bbb"] def test_matching_caches_are_used_without_fetch(self): @@ -599,7 +599,7 @@ class TestSourceCacheIntegrity(OpenpilotTestCase): fetcher = ModelFetcher(params) with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")): assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] - assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"] + assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"] def test_stale_version_cache_is_refetched(self): """A source-matching cache whose bundles are all filtered by the selector version @@ -660,28 +660,28 @@ class TestActiveBundleValidation(OpenpilotTestCase): bundle.runner = runner return bundle.to_dict() - def _params(self, qcom=None, usbgpu=None): + def _params(self, qcom=None, chestnut=None): params = mock.MagicMock() def get(key, *args, **kwargs): - return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleUSBGPU": usbgpu}.get(key) + return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleChestnut": chestnut}.get(key) params.get.side_effect = get return params def test_empty_catalog_does_not_reset_slot(self): params = self._params(qcom=self._raw_bundle("small")) - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): - validate_active_bundles(params, {"qcom": [], "usbgpu": []}) + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): + validate_active_bundles(params, {"qcom": [], "chestnut": []}) params.remove.assert_not_called() def test_reset_recomputes_runner_from_surviving_slot(self): tinygrad = int(custom.ModelManagerSP.Runner.tinygrad) big_raw = self._raw_bundle("big", runner=tinygrad) - params = self._params(qcom=self._raw_bundle("gone"), usbgpu=big_raw) + params = self._params(qcom=self._raw_bundle("gone"), chestnut=big_raw) catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))], - "usbgpu": [custom.ModelManagerSP.ModelBundle(**big_raw)]} - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + "chestnut": [custom.ModelManagerSP.ModelBundle(**big_raw)]} + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): validate_active_bundles(params, catalog) params.remove.assert_called_once_with("ModelManager_ActiveBundle") runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"] @@ -689,7 +689,7 @@ class TestActiveBundleValidation(OpenpilotTestCase): class TestActiveBundleSelection(OpenpilotTestCase): - """The effective active bundle is the active source's slot: usbgpu when a GPU is + """The effective active bundle is the active source's slot: chestnut when a GPU is present, qcom otherwise. An empty active slot means the hardware default (stock runner), never the other slot's pick - modeld_v2 requires a real bundle.""" @@ -700,37 +700,37 @@ class TestActiveBundleSelection(OpenpilotTestCase): bundle.minimumSelectorVersion = 18 return bundle.to_dict() - def _params(self, qcom=None, usbgpu=None): + def _params(self, qcom=None, chestnut=None): params = mock.MagicMock() def get(key, *args, **kwargs): if key == "ModelManager_ActiveBundle": return qcom - if key == "ModelManager_ActiveBundleUSBGPU": - return usbgpu + if key == "ModelManager_ActiveBundleChestnut": + return chestnut return None params.get.side_effect = get return params def test_selected_bundle_is_per_slot(self): - params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) + params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big")) assert get_selected_bundle(params, "qcom").ref == "small" - assert get_selected_bundle(params, "usbgpu").ref == "big" + assert get_selected_bundle(params, "chestnut").ref == "big" def test_no_gpu_uses_qcom_slot(self): - params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big")) + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): assert get_active_bundle(params).ref == "small" - def test_gpu_uses_usbgpu_slot(self): - params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + def test_gpu_uses_chestnut_slot(self): + params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big")) + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): assert get_active_bundle(params).ref == "big" def test_gpu_without_big_selection_is_hardware_default(self): - params = self._params(qcom=self._raw_bundle("small"), usbgpu=None) - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + params = self._params(qcom=self._raw_bundle("small"), chestnut=None) + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): assert get_active_bundle(params) is None @@ -748,36 +748,36 @@ class TestEffectiveSource(OpenpilotTestCase): return bundle.to_dict() def test_runtime_no_gpu(self): - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): assert get_active_source() == "qcom" def test_runtime_gpu_present(self): - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): - assert get_active_source() == "usbgpu" + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): + assert get_active_source() == "chestnut" def test_display_offroad_gpu_present_shows_big(self): - assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=True) == "usbgpu" + assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=True) == "chestnut" def test_display_onroad_gpu_loading_shows_big(self): - assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=True, offroad=False) == "usbgpu" + assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=True, offroad=False) == "chestnut" def test_display_onroad_gpu_active_shows_big(self): - assert get_active_source(usbgpu=True, usbgpu_active=True, usbgpu_loading=False, offroad=False) == "usbgpu" + assert get_active_source(chestnut=True, chestnut_active=True, chestnut_loading=False, offroad=False) == "chestnut" def test_display_onroad_gpu_idle_shows_small(self): - assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=False) == "qcom" + assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=False) == "qcom" def test_display_active_none_is_idle(self): - assert get_active_source(usbgpu=True, usbgpu_active=None, usbgpu_loading=False, offroad=False) == "qcom" + assert get_active_source(chestnut=True, chestnut_active=None, chestnut_loading=False, offroad=False) == "qcom" def test_active_bundle_follows_source(self): params = mock.MagicMock() params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"), - "ModelManager_ActiveBundleUSBGPU": self._raw_bundle("big")}.get(key) - with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + "ModelManager_ActiveBundleChestnut": self._raw_bundle("big")}.get(key) + with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): assert get_active_bundle(params).ref == "small" - assert get_selected_bundle(params, get_active_source(usbgpu=True, usbgpu_active=False, - usbgpu_loading=False, offroad=True)).ref == "big" + assert get_selected_bundle(params, get_active_source(chestnut=True, chestnut_active=False, + chestnut_loading=False, offroad=True)).ref == "big" @unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network') diff --git a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index 1ab2f373e..c31e4ac71 100755 --- a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -183,7 +183,7 @@ def getParamsMetadata() -> str: schema["capability_labels"] = CAPABILITY_LABELS schema["default_model"] = DEFAULT_MODEL schema["default_big_model"] = DEFAULT_BIG_MODEL - schema["usbgpu_active"] = params.get_bool("UsbGpuActive") + schema["chestnut_active"] = params.get_bool("ChestnutActive") raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8") except Exception: diff --git a/openpilot/sunnypilot/sunnylink/statsd.py b/openpilot/sunnypilot/sunnylink/statsd.py index a221fc084..9d01ac07f 100755 --- a/openpilot/sunnypilot/sunnylink/statsd.py +++ b/openpilot/sunnypilot/sunnylink/statsd.py @@ -65,7 +65,7 @@ def sp_stats(end_event): 'MadsSteeringMode', 'MadsUnifiedEngagementMode', 'ModelManager_ActiveBundle', - 'ModelManager_ActiveBundleUSBGPU', + 'ModelManager_ActiveBundleChestnut', 'ModelManager_Favs', 'EnableSunnylinkUploader', 'SunnylinkEnabled', diff --git a/openpilot/sunnypilot/system/params_migration.py b/openpilot/sunnypilot/system/params_migration.py index f0f0d7248..41cad9ad2 100644 --- a/openpilot/sunnypilot/system/params_migration.py +++ b/openpilot/sunnypilot/system/params_migration.py @@ -89,12 +89,16 @@ def _migrate_model_bundle_slots(_params): # ActiveBundle. Seed both slots; validation drops whichever does not match # its own manifest. try: - if _params.get("ModelManager_ActiveBundleUSBGPU") is not None: + if _params.get("ModelManager_ActiveBundleChestnut") is not None: + return + if (chestnut_bundle := _params.get("ModelManager_ActiveBundleUSBGPU")) is not None: + _params.put("ModelManager_ActiveBundleChestnut", chestnut_bundle, block=True) + cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundleUSBGPU") return if (bundle := _params.get("ModelManager_ActiveBundle")) is None: return - _params.put("ModelManager_ActiveBundleUSBGPU", bundle, block=True) - cloudlog.info("params_migration: seeded ModelManager_ActiveBundleUSBGPU from ModelManager_ActiveBundle") + _params.put("ModelManager_ActiveBundleChestnut", bundle, block=True) + cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundle") except Exception as e: cloudlog.exception(f"Error migrating model bundle slots: {e}") @@ -136,5 +140,5 @@ def run_migration(_params): # seed TeslaMadsScreenButton for existing Tesla installs _migrate_tesla_mads_screen_button(_params) - # seed the usbgpu model slot from the pre-split single slot + # seed the chestnut model slot from the pre-split single slot _migrate_model_bundle_slots(_params) diff --git a/openpilot/sunnypilot/system/tests/test_params_migration.py b/openpilot/sunnypilot/system/tests/test_params_migration.py index 328a7a65a..683c9dcdc 100644 --- a/openpilot/sunnypilot/system/tests/test_params_migration.py +++ b/openpilot/sunnypilot/system/tests/test_params_migration.py @@ -15,22 +15,22 @@ class TestModelBundleSlotMigration(OpenpilotTestCase): The migration seeds both slots; per-source validation later drops whichever does not match its own manifest.""" - def test_seeds_usbgpu_slot_from_active_bundle(self): + def test_seeds_chestnut_slot_from_active_bundle(self): params = Params() bundle = {"ref": "big", "minimumSelectorVersion": 18} params.put("ModelManager_ActiveBundle", bundle, block=True) _migrate_model_bundle_slots(params) - assert params.get("ModelManager_ActiveBundleUSBGPU") == bundle + assert params.get("ModelManager_ActiveBundleChestnut") == bundle assert params.get("ModelManager_ActiveBundle") == bundle - def test_noop_when_usbgpu_slot_already_set(self): + def test_noop_when_chestnut_slot_already_set(self): params = Params() params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True) - params.put("ModelManager_ActiveBundleUSBGPU", {"ref": "big"}, block=True) + params.put("ModelManager_ActiveBundleChestnut", {"ref": "big"}, block=True) _migrate_model_bundle_slots(params) - assert params.get("ModelManager_ActiveBundleUSBGPU") == {"ref": "big"} + assert params.get("ModelManager_ActiveBundleChestnut") == {"ref": "big"} def test_noop_when_no_selection(self): params = Params() _migrate_model_bundle_slots(params) - assert params.get("ModelManager_ActiveBundleUSBGPU") is None + assert params.get("ModelManager_ActiveBundleChestnut") is None diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 475bde3f4..d0fbd0fac 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,7 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW -from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.basedir import BASEDIR @@ -239,7 +239,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled() + big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT)