Compare commits

..

1 Commits

Author SHA1 Message Date
nayan d68a6cd87d zoom zoom 2026-08-28 21:11:29 -04:00
582 changed files with 16136 additions and 26212 deletions
+1 -3
View File
@@ -1,10 +1,8 @@
* text=auto eol=lf * text=auto
# to move existing files into LFS: # to move existing files into LFS:
# git add --renormalize . # git add --renormalize .
*.onnx filter=lfs diff=lfs merge=lfs -text *.onnx filter=lfs diff=lfs merge=lfs -text
openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text
openpilot/sunnypilot/modeld_v2/models/*.pkl filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text *.svg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text
+11
View File
@@ -0,0 +1,11 @@
* @sunnypilot/dev-internal
/.github/ @devtekve @sunnyhaibin
/release/ci/ @devtekve @sunnyhaibin
/tinygrad_repo @devtekve @Discountchubbs
/tinygrad/ @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
/selfdrive/modeld/ @devtekve @Discountchubbs
/sunnypilot/model* @devtekve @Discountchubbs
/sunnypilot/sunnylink/ @devtekve
/system/athena/ @devtekve
+43
View File
@@ -0,0 +1,43 @@
exclude-labels:
- 'no-changelog'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
collapse-after: 5
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
collapse-after: 5
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&'
replacers:
- search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g'
replace: 'sunnypilot'
- search: '/\b[Ss][Pp]\b/g'
replace: 'SP'
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
name-template: 'v$RESOLVED_VERSION 🚀'
tag-template: 'v$RESOLVED_VERSION'
version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this
tag-prefix: "v0." # The day OP becomes v1, we need to bump this
prerelease-identifier: "staging"
template: |
## Changes
$CHANGES
@@ -8,23 +8,18 @@ on:
required: true required: true
type: string type: string
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)' description: 'Hardware target to compile for (qcom or usbgpu)'
required: true required: true
type: choice type: choice
default: 'qcom' default: 'qcom'
options: options:
- qcom - qcom
- chestnut - usbgpu
hf_repo: hf_repo:
description: 'Hugging Face dataset repository' description: 'Hugging Face dataset repository'
required: false required: false
type: string type: string
default: 'sunnypilot/sunnypilot_models_v1' default: 'sunnypilot/sunnypilot_models_v1'
docs_repo:
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
required: false
type: string
default: 'sunnypilot/sunnypilot-models'
jobs: jobs:
setup: setup:
@@ -39,9 +34,9 @@ jobs:
- name: Checkout sunnypilot repo - name: Checkout sunnypilot repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: sunnypilot/sunnypilot
path: sunnypilot path: sunnypilot
submodules: recursive submodules: recursive
fetch-depth: 1
- name: Get tinygrad_repo ref - name: Get tinygrad_repo ref
id: get-tinygrad-ref id: get-tinygrad-ref
@@ -52,20 +47,19 @@ jobs:
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
echo "tinygrad_ref is $ref" echo "tinygrad_ref is $ref"
- name: Checkout docs repo (gh-pages) - name: Checkout docs repo (sunnypilot-models, gh-pages)
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: ${{ inputs.docs_repo }} repository: sunnypilot/sunnypilot-models
ref: gh-pages ref: gh-pages
path: docs path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
fetch-depth: 1
- name: Get next JSON version to use (from GitHub docs repo) - name: Get next JSON version to use (from GitHub docs repo)
id: get-json id: get-json
run: | run: |
cd docs/docs cd docs/docs
PREFIX="driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_' || '' }}v" PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v"
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1) latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
next=$((latest+1)) next=$((latest+1))
json_file="${PREFIX}${next}.json" json_file="${PREFIX}${next}.json"
@@ -84,7 +78,6 @@ jobs:
- name: Get next recompiled dir number - name: Get next recompiled dir number
id: create-recompiled-dir id: create-recompiled-dir
env: env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_REPO: ${{ github.event.inputs.hf_repo }} HF_REPO: ${{ github.event.inputs.hf_repo }}
run: | run: |
pip install huggingface_hub pip install huggingface_hub
@@ -124,7 +117,6 @@ jobs:
json_version: ${{ needs.setup.outputs.json_version }} json_version: ${{ needs.setup.outputs.json_version }}
target_hardware: ${{ github.event.inputs.target_hardware }} target_hardware: ${{ github.event.inputs.target_hardware }}
hf_repo: ${{ github.event.inputs.hf_repo }} hf_repo: ${{ github.event.inputs.hf_repo }}
docs_repo: ${{ inputs.docs_repo }}
set_min_version: ${{ github.event.inputs.set_min_version }} set_min_version: ${{ github.event.inputs.set_min_version }}
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }} tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
secrets: inherit secrets: inherit
@@ -169,7 +161,6 @@ jobs:
target_hardware: ${{ github.event.inputs.target_hardware }} target_hardware: ${{ github.event.inputs.target_hardware }}
artifact_suffix: -retry artifact_suffix: -retry
hf_repo: ${{ github.event.inputs.hf_repo }} hf_repo: ${{ github.event.inputs.hf_repo }}
docs_repo: ${{ inputs.docs_repo }}
set_min_version: ${{ github.event.inputs.set_min_version }} set_min_version: ${{ github.event.inputs.set_min_version }}
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }} tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
secrets: inherit secrets: inherit
+111 -103
View File
@@ -30,7 +30,6 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
outputs: outputs:
model_name: ${{ steps.resolve.outputs.model_name }} model_name: ${{ steps.resolve.outputs.model_name }}
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }} onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
onnx_path: ${{ steps.resolve.outputs.onnx_path }} onnx_path: ${{ steps.resolve.outputs.onnx_path }}
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }} hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
@@ -46,8 +45,8 @@ jobs:
if [ "${{ inputs.target }}" = "big" ]; then if [ "${{ inputs.target }}" = "big" ]; then
NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)") 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" HF_DEFAULTS_PATH="models/defaults/big"
ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl"
elif [ "${{ inputs.target }}" = "dm" ]; then elif [ "${{ inputs.target }}" = "dm" ]; then
ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx"
HF_DEFAULTS_PATH="models/defaults/dm" HF_DEFAULTS_PATH="models/defaults/dm"
@@ -57,18 +56,15 @@ jobs:
ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx" ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx"
HF_DEFAULTS_PATH="models/defaults/small" HF_DEFAULTS_PATH="models/defaults/small"
fi fi
ONNX_REF=""
[ -n "$ONNX_PATH" ] && ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH")
ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH")
TINYGRAD_REF=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py) TINYGRAD_REF=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)
if [ -z "$TINYGRAD_REF" ]; then if [ -z "$TINYGRAD_REF" ]; then
echo "::error::Failed to resolve tinygrad ref" echo "::error::Failed to resolve tinygrad ref"
exit 1 exit 1
fi fi
SAFE_NAME="${NAME// /-}"
echo "model_name=${NAME}" >> $GITHUB_OUTPUT echo "model_name=${NAME}" >> $GITHUB_OUTPUT
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
@@ -79,8 +75,6 @@ jobs:
if: ${{ inputs.target == 'small' }} if: ${{ inputs.target == 'small' }}
runs-on: [self-hosted, tici] runs-on: [self-hosted, tici]
env: env:
MODELS_DIR: openpilot/selfdrive/modeld/models
COMPILER: tinygrad_repo/examples/openpilot
SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx
SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl
steps: steps:
@@ -88,10 +82,9 @@ jobs:
with: with:
submodules: recursive submodules: recursive
- name: Pull small ONNX via LFS - name: Pull ONNX via LFS
run: git lfs pull -I "${{ env.SMALL_ONNX }}" run: git lfs pull -I "${{ env.SMALL_ONNX }}"
- name: Set environment variables - name: Set environment variables
run: | run: |
source /etc/profile source /etc/profile
@@ -107,19 +100,42 @@ jobs:
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
- name: Compile small model from ONNX - name: Compile small model with stock compiler
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" 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" TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
env ${TG_FLAGS} python3 ${{ github.workspace }}/${{ env.COMPILER }}/compile_onnx.py \
${{ github.workspace }}/${{ env.SMALL_ONNX }} \ env ${TG_FLAGS} python3 \
${{ github.workspace }}/${{ env.SMALL_PKL }} \ ${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \
--out-of-band --benchmark-runs 1 --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 - name: Prepare output
env: env:
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }} MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }} export PYTHONPATH=${{ github.workspace }}
@@ -128,29 +144,27 @@ jobs:
PKL_BASE="driving_tinygrad.pkl" PKL_BASE="driving_tinygrad.pkl"
mkdir -p "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
cp "$MODELS_DIR/${PKL_BASE}" "$OUTPUT_DIR/" cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
ONNX_SHA256=$(git cat-file blob "$(git ls-files -s "${{ github.workspace }}/${{ env.SMALL_ONNX }}" | awk '{print $2}')" | grep '^oid sha256:' | cut -d: -f2)
python3 "${{ github.workspace }}/release/ci/model_generator.py" \ python3 "${{ github.workspace }}/release/ci/model_generator.py" \
--model-dir "$MODELS_DIR" \ --model-dir "$MODELS_DIR" \
--output-dir "$OUTPUT_DIR" \ --output-dir "$OUTPUT_DIR" \
--custom-name "$MODEL_NAME" \ --custom-name "$MODEL_NAME" \
--upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" \ --upstream-branch "${{ needs.resolve.outputs.onnx_ref }}"
--onnx-sha256 "$ONNX_SHA256"
echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt" echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt"
- name: Upload small model artifact - name: Upload small model artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }} name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/small_output/ path: ${{ github.workspace }}/small_output/
- name: Upload artifact name file - name: Upload artifact name file
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }} name: artifact-name-${{ needs.resolve.outputs.model_name }}
path: ${{ github.workspace }}/small_output/artifact_name.txt path: ${{ github.workspace }}/small_output/artifact_name.txt
- name: Re-enable powersave - name: Re-enable powersave
@@ -162,18 +176,17 @@ jobs:
build_big_model: build_big_model:
needs: resolve needs: resolve
if: ${{ inputs.target == 'big' }} if: ${{ inputs.target == 'big' }}
runs-on: [self-hosted, chestnut] runs-on: [self-hosted, usbgpu]
env: env:
MODELS_DIR: openpilot/selfdrive/modeld/models BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
COMPILER: tinygrad_repo/examples/openpilot
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- name: Pull big PKL via LFS - name: Pull big ONNX via LFS
run: git lfs pull -I "${{ env.BIG_PKL }}" run: git lfs pull -I "${{ env.BIG_ONNX }}"
- name: Set environment variables - name: Set environment variables
run: | run: |
@@ -206,37 +219,42 @@ jobs:
raise RuntimeError('Chestnut PCIe link not ready after 10 attempts') raise RuntimeError('Chestnut PCIe link not ready after 10 attempts')
" "
- name: Compile big warp - name: Compile big model with stock compiler
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
CHESTNUT_TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32"
COMPILER="${{ github.workspace }}/tinygrad_repo/examples/openpilot"
MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models"
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") 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)")
for res in $(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}')"); do TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
CAM_W=$(echo $res | cut -d x -f1)
CAM_H=$(echo $res | cut -d x -f2) env ${TG_FLAGS} python3 \
STRIDE_INFO=$(python3 -c " ${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \
import sys --onnx ${{ github.workspace }}/${{ env.BIG_ONNX }} \
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info --model-size $MODEL_SIZE \
s,y,u,sz = get_nv12_info(int(sys.argv[1]), int(sys.argv[2])) --camera-resolutions $CAMERA_RES \
print(f'{s},{y},{u},{sz}') --frame-skip $FRAME_SKIP \
" $CAM_W $CAM_H) --output ${{ github.workspace }}/${{ env.BIG_PKL }}
WARP_PKL="${MODELS_DIR}/big_driving_warp_${res}_tinygrad.pkl"
env ${CHESTNUT_TG_FLAGS} python3 "${COMPILER}/compile_warp.py" \ - name: Chunk big pkl
--frame ${CAM_W},${CAM_H},${STRIDE_INFO} \ run: |
--warp-to ${MODEL_SIZE} \ source ${UV_PROJECT_ENVIRONMENT}/bin/activate
--layout yuv420 \ export PYTHONPATH=${{ github.workspace }}
--frames 2 \ python3 -c "
--output "${WARP_PKL}" from openpilot.common.file_chunker import chunk_file, get_chunk_targets
done 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 - name: Prepare output
env: env:
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }} MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }} export PYTHONPATH=${{ github.workspace }}
@@ -245,31 +263,27 @@ jobs:
PKL_BASE="big_driving_tinygrad.pkl" PKL_BASE="big_driving_tinygrad.pkl"
mkdir -p "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
cp "$MODELS_DIR/${PKL_BASE}" "$OUTPUT_DIR/" cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
PKL_LFS_SHA256=$(git cat-file blob "$(git ls-files -s "${{ github.workspace }}/${{ env.BIG_PKL }}" | awk '{print $2}')" | grep '^oid sha256:' | cut -d: -f2)
python3 "${{ github.workspace }}/release/ci/model_generator.py" \ python3 "${{ github.workspace }}/release/ci/model_generator.py" \
--model-dir "$MODELS_DIR" \ --model-dir "$MODELS_DIR" \
--output-dir "$OUTPUT_DIR" \ --output-dir "$OUTPUT_DIR" \
--custom-name "$MODEL_NAME" \ --custom-name "$MODEL_NAME" \
--upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" \ --upstream-branch "${{ needs.resolve.outputs.onnx_ref }}"
--onnx-sha256 "$PKL_LFS_SHA256"
cp "$MODELS_DIR"/big_driving_warp_* "$OUTPUT_DIR/" || true
echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt" echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt"
- name: Upload big model artifact - name: Upload big model artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }} name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/big_output/ path: ${{ github.workspace }}/big_output/
- name: Upload artifact name file - name: Upload artifact name file
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }} name: artifact-name-${{ needs.resolve.outputs.model_name }}
path: ${{ github.workspace }}/big_output/artifact_name.txt path: ${{ github.workspace }}/big_output/artifact_name.txt
- name: Re-enable powersave - name: Re-enable powersave
@@ -294,8 +308,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Pull LFS for resolved ONNX files - name: Pull ONNX via LFS
if: ${{ needs.resolve.outputs.onnx_path != '' }}
run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}" run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}"
- name: Install huggingface_hub - name: Install huggingface_hub
@@ -305,7 +318,7 @@ jobs:
if: ${{ inputs.target == 'small' || inputs.target == 'big' }} if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }} name: artifact-name-${{ needs.resolve.outputs.model_name }}
path: artifact_name path: artifact_name
- name: Read artifact name - name: Read artifact name
@@ -325,7 +338,7 @@ jobs:
- name: Upload model to HF - name: Upload model to HF
if: ${{ inputs.target == 'small' || inputs.target == 'big' }} if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
env: env:
HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
run: | run: |
rm -f output/artifact_name.txt rm -f output/artifact_name.txt
@@ -351,7 +364,7 @@ jobs:
- name: Generate DM metadata and upload to HF - name: Generate DM metadata and upload to HF
if: ${{ inputs.target == 'dm' }} if: ${{ inputs.target == 'dm' }}
env: env:
HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
run: | run: |
export PYTHONPATH=$(pwd) export PYTHONPATH=$(pwd)
python3 -c " python3 -c "
@@ -360,28 +373,38 @@ jobs:
from datetime import datetime, UTC from datetime import datetime, UTC
dm_dir = Path('dm_output') dm_dir = Path('dm_output')
matches = sorted(dm_dir.glob('dmonitoring_*_tinygrad.pkl')) manifest = list(dm_dir.glob('*.chunkmanifest'))
assert len(matches) == 1, f'expected one DM pkl in {dm_dir}, found {matches}' assert manifest, 'No chunkmanifest found'
pkl_name = matches[0].name 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() digest = hashlib.sha256()
with open(dm_dir / pkl_name, 'rb') as f: for c in chunks:
while block := f.read(1024*1024): with open(dm_dir / c['file_name'], 'rb') as f:
digest.update(block) while block := f.read(1024*1024):
digest.update(block)
metadata = { metadata = {
'bundles': [{ 'bundles': [{
'short_name': 'DMMODEL', 'short_name': 'DMMODEL',
'display_name': '${{ needs.resolve.outputs.model_name }}', 'display_name': '${{ needs.resolve.outputs.model_name }}',
'ref': '${{ needs.resolve.outputs.onnx_ref }}', 'ref': '${{ needs.resolve.outputs.onnx_ref }}',
'tinygrad_ref': '${{ needs.resolve.outputs.tinygrad_ref }}',
'runner': 'tinygrad', 'runner': 'tinygrad',
'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), 'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'),
'models': [{ 'models': [{
'type': 'chunked', 'type': 'chunked',
'artifact': { 'artifact': {
'file_name': pkl_name, 'file_name': pkl_name,
'download_uri': {'url': '', 'sha256': digest.hexdigest()} 'download_uri': {'url': '', 'sha256': digest.hexdigest()},
'chunks': chunks
} }
}] }]
}] }]
@@ -440,45 +463,29 @@ jobs:
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
taskset -c 7 env ${TG_FLAGS} python3 \ taskset -c 7 env ${TG_FLAGS} python3 \
${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile_onnx.py \ ${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile3.py \
${{ github.workspace }}/${{ env.DM_ONNX }} \ ${{ github.workspace }}/${{ env.DM_ONNX }} \
${{ github.workspace }}/${{ env.DM_PKL }} \ ${{ github.workspace }}/${{ env.DM_PKL }}
--out-of-band --benchmark-runs 1
- name: Compile DM warp - name: Chunk DM pkl
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" export PYTHONPATH=${{ github.workspace }}
python3 -c "
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" from openpilot.common.file_chunker import chunk_file, get_chunk_targets
COMPILER="${{ github.workspace }}/tinygrad_repo/examples/openpilot" import os
MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models" pkl = '${{ github.workspace }}/${{ env.DM_PKL }}'
DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") size = os.path.getsize(pkl)
targets = get_chunk_targets(pkl, size)
for res in $(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}')"); do chunk_file(pkl, targets)
CAM_W=$(echo $res | cut -d x -f1) print(f'Chunked {pkl} into {len(targets)} chunks')
CAM_H=$(echo $res | cut -d x -f2) "
STRIDE_INFO=$(python3 -c "
import sys
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
s,y,u,sz = get_nv12_info(int(sys.argv[1]), int(sys.argv[2]))
print(f'{s},{y},{u},{sz}')
" $CAM_W $CAM_H)
WARP_PKL="${MODELS_DIR}/dm_warp_${res}_tinygrad.pkl"
taskset -c 7 env ${TG_FLAGS} python3 "${COMPILER}/compile_warp.py" \
--frame ${CAM_W},${CAM_H},${STRIDE_INFO} \
--warp-to ${DM_SIZE} \
--layout luma \
--border-fill 16 \
--transform-device NPY \
--output "${WARP_PKL}"
done
- name: Prepare DM output - name: Prepare DM output
run: | run: |
mkdir -p dm_output mkdir -p dm_output
cp ${{ github.workspace }}/${{ env.DM_PKL }} dm_output/ cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/ cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
- name: Upload DM artifact - name: Upload DM artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -491,3 +498,4 @@ jobs:
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
@@ -30,7 +30,7 @@ on:
type: boolean type: boolean
default: true default: true
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)' description: 'Hardware target to compile for (qcom or usbgpu)'
required: false required: false
type: string type: string
default: 'qcom' default: 'qcom'
@@ -39,11 +39,6 @@ on:
required: false required: false
type: string type: string
default: 'sunnypilot/sunnypilot_models_v1' default: 'sunnypilot/sunnypilot_models_v1'
docs_repo:
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
required: false
type: string
default: 'sunnypilot/sunnypilot-models'
set_min_version: set_min_version:
description: 'Minimum selector version' description: 'Minimum selector version'
required: false required: false
@@ -106,20 +101,15 @@ on:
default: 'qcom' default: 'qcom'
options: options:
- qcom - qcom
- chestnut - usbgpu
hf_repo: hf_repo:
description: 'Hugging Face dataset repository' description: 'Hugging Face dataset repository'
required: false required: false
type: string type: string
default: 'sunnypilot/sunnypilot_models_v1' default: 'sunnypilot/sunnypilot_models_v1'
docs_repo:
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
required: false
type: string
default: 'sunnypilot/sunnypilot-models'
env: env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json
jobs: jobs:
build_model: build_model:
@@ -134,17 +124,19 @@ jobs:
publish_model: publish_model:
if: ${{ !cancelled() && needs.build_model.result == 'success' }} if: ${{ !cancelled() && needs.build_model.result == 'success' }}
concurrency:
group: hf-push-${{ inputs.recompiled_dir }}
cancel-in-progress: false
needs: build_model needs: build_model
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read
id-token: write id-token: write
contents: write contents: write
steps: steps:
- name: Checkout docs repo - name: Checkout docs repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: ${{ inputs.docs_repo }} repository: sunnypilot/sunnypilot-models
ref: gh-pages ref: gh-pages
path: docs path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
@@ -154,7 +146,7 @@ jobs:
- name: Validate hf_repo and JSON version - name: Validate hf_repo and JSON version
env: env:
HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
run: | run: |
if [ ! -f "$JSON_FILE" ]; then if [ ! -f "$JSON_FILE" ]; then
echo "JSON file $JSON_FILE does not exist!" echo "JSON file $JSON_FILE does not exist!"
@@ -163,8 +155,13 @@ jobs:
python3 -c " python3 -c "
import sys import sys
from huggingface_hub import HfApi from huggingface_hub import HfApi
HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset') try:
print(f'Success: Repo {sys.argv[1]} exists.') 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 }}" " "${{ inputs.hf_repo }}"
- name: Download artifact name file - name: Download artifact name file
@@ -180,25 +177,22 @@ jobs:
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
- name: Download and extract model artifact - name: Download and extract model artifact
env: uses: actions/download-artifact@v4
GH_TOKEN: ${{ github.token }} with:
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} name: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: | path: output
id=$(gh api -X GET "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts" -f name="$ARTIFACT_NAME" --jq '.artifacts[0].id')
url=$(curl -sf -o /dev/null -w '%{redirect_url}' -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip")
sudo apt-get install -y -qq aria2 > /dev/null
aria2c -q -x16 -s16 -k8M --max-tries=5 --file-allocation=none -o model.zip "$url" && unzip -q model.zip -d output && rm model.zip
- name: Create models folder - name: Create models folder
env: env:
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: | run: |
mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}"
cp output/metadata.json "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 - name: Upload to Hugging Face
env: env:
HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: | run: |
hf upload ${{ inputs.hf_repo }} \ hf upload ${{ inputs.hf_repo }} \
@@ -206,12 +200,13 @@ jobs:
"models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \ "models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \
--repo-type=dataset --repo-type=dataset
- name: Update and push JSON to docs repo - name: Pull gh-pages
run: | run: |
cd docs cd docs
git config --global user.name "GitHub Action" git pull origin gh-pages
git config --global user.email "action@github.com"
- name: Run json_parser.py to update JSON
run: |
FOLDER="${{ inputs.model_folder }}" FOLDER="${{ inputs.model_folder }}"
if [ "$FOLDER" = "Other" ]; then if [ "$FOLDER" = "Other" ]; then
FOLDER="${{ inputs.custom_model_folder }}" FOLDER="${{ inputs.custom_model_folder }}"
@@ -224,27 +219,18 @@ jobs:
[ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\"" [ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\""
[ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_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 }}\"" [ -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
for attempt in $(seq 1 10); do - name: Push updated JSON to GitHub docs repo
git fetch origin gh-pages run: |
git reset --hard origin/gh-pages cd docs
git config --global user.name "GitHub Action"
eval python3 json_parser.py \ git config --global user.email "action@github.com"
--json-path "docs/$(basename $JSON_FILE)" \ git checkout gh-pages
--recompiled-dir "../local_models/$RECOMPILED_DIR" \ git add docs/"$(basename $JSON_FILE)"
--sort-by-date \ git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit"
$ARGS git push origin gh-pages
git add docs/"$(basename $JSON_FILE)"
git commit -m "Update $(basename $JSON_FILE) after recompiling model" || { echo "No changes to commit"; exit 0; }
if git push origin gh-pages; then
echo "Push succeeded on attempt $attempt"
exit 0
fi
echo "Push failed (attempt $attempt/10), retrying after backoff..."
sleep $((RANDOM % 10 + 3))
done
echo "All 10 push attempts failed"
exit 1
@@ -1,80 +0,0 @@
name: Compile warp for sunnypilot modeld
on:
workflow_dispatch:
jobs:
compile_warps:
runs-on: [self-hosted, chestnut]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- 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 Warp Kernels
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
mkdir -p openpilot/sunnypilot/modeld_v2/models/
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}')")
echo "Model size: $MODEL_SIZE"
echo "Camera resolutions: $CAMERA_RES"
for res in $CAMERA_RES; do
FRAME_PARAM=$(python3 -c "from openpilot.system.camerad.cameras.nv12_info import get_nv12_info; w, h = map(int, '${res}'.split('x')); stride, y_h, uv_h, _ = get_nv12_info(w, h); print(f'{w},{h},{stride},{y_h},{uv_h},{stride * (y_h + uv_h)}')")
WARP_PKL="openpilot/sunnypilot/modeld_v2/models/driving_warp_${res}_tinygrad.pkl"
echo "Compiling $WARP_PKL on QCOM"
taskset -c 7 env DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1 python3 openpilot/sunnypilot/modeld_v2/compile_warp.py \
--frame ${FRAME_PARAM} --warp-to ${MODEL_SIZE} --output "${WARP_PKL}"
BIG_WARP_PKL="openpilot/sunnypilot/modeld_v2/models/big_driving_warp_${res}_tinygrad.pkl"
echo "Compiling $BIG_WARP_PKL on AMD"
taskset -c 7 env DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32 python3 openpilot/sunnypilot/modeld_v2/compile_warp.py \
--frame ${FRAME_PARAM} --warp-to ${MODEL_SIZE} --output "${BIG_WARP_PKL}"
done
- name: Re-enable powersave
if: always()
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: credential.https://huggingface.co.helper
GIT_CONFIG_VALUE_0: '!f() { test "$1" = get && echo username=user && echo "password=$HF_TOKEN"; }; f'
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "[bot] Update warp pkl for modeld_v2"
title: "[bot] Update modeld_v2 Warp"
branch: "auto/compile-warp-kernels"
base: "master"
delete-branch: true
labels: bot
add-paths: |
openpilot/sunnypilot/modeld_v2/models/*.pkl
- name: Reset lfs access
if: always()
run: git config --local --unset-all "lfs.$(git config -f .lfsconfig lfs.url).access" || true
+8 -9
View File
@@ -18,25 +18,24 @@ concurrency:
env: env:
GIT_CONFIG_COUNT: 1 GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs: jobs:
docs: docs:
name: build docs name: build docs
runs-on: ${{ runs-on: ubuntu-24.04
(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: steps:
- uses: commaai/timeout@v1 - uses: commaai/timeout@v1
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- run: ./tools/op.sh setup with:
submodules: true
# Build # Build
- name: Build docs - name: Build docs
run: ./tools/op.sh docs --build run: |
git lfs pull
python docs/serve.py --build
# Push to docs.comma.ai # Push to docs.comma.ai
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -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"
@@ -1,89 +0,0 @@
name: Download HF model
description: Resolve and download models 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
tinygrad_ref:
description: 'Tinygrad ref to match bundle against'
required: false
default: ''
runs:
using: composite
steps:
- name: Download models
shell: bash
env:
HF_REPO: ${{ inputs.hf_repo }}
MODELS_JSON: ${{ inputs.models }}
DEST_DIR: ${{ inputs.dest_dir }}
TINYGRAD_REF: ${{ inputs.tinygrad_ref }}
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 FILE_URL BASE_URL
DEFAULTS=$(curl -fsSL "$JSON_URL")
if [ -n "$TINYGRAD_REF" ]; then
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" --arg tg "$TINYGRAD_REF" '
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $tg or (.tinygrad_ref == null and $root.tinygrad_ref == $tg)))] | .[0] // empty
')
else
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '
[.bundles[] | select(.onnx_sha256 == $hash)] | .[0] // empty
')
fi
if [ -z "$BUNDLE" ] || [ "$BUNDLE" = "null" ]; then
echo "::error::No model bundle found matching ONNX hash $ONNX_HASH (tinygrad ref: $TINYGRAD_REF) in $HF_PATH"
return 1
fi
ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact')
FILE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url')
BASE_URL=$(echo "$FILE_URL" | sed 's|/[^/]*$||')
mkdir -p "$DEST_DIR"
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${FILE_URL}', safe=':/'))")
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}" >> "$DOWNLOAD_LIST"
if [ "$CANONICAL" = "dmonitoring_model_tinygrad.pkl" ]; then
for warp in dm_warp_1928x1208_tinygrad.pkl dm_warp_1344x760_tinygrad.pkl; do
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))")
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST"
done
elif [ "$CANONICAL" = "big_driving_tinygrad.pkl" ]; then
for warp in big_driving_warp_1928x1208_tinygrad.pkl big_driving_warp_1344x760_tinygrad.pkl; do
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))")
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST"
done
fi
}
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 files 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"
+10 -86
View File
@@ -1,8 +1,8 @@
name: Sync comma's LFS name: Sync comma's LFS
env: env:
LFS_URL: 'https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs' LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs'
LFS_PUSH_URL: 'https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs' LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git'
on: on:
schedule: schedule:
@@ -33,19 +33,21 @@ jobs:
- name: LFS Fetch - name: LFS Fetch
run: | run: |
git lfs fetch || echo "::warning::some objects are missing on commaai/openpilot-lfs; mirroring the rest" git lfs fetch
- name: Set up Git - name: Set up Git
run: | run: |
git config --global user.name 'GitHub Action' git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com' git config --global user.email 'action@github.com'
- name: Setup hf - name: Set up SSH
env: uses: webfactory/ssh-agent@v0.9.0
HF_TOKEN: ${{ secrets.HF_TOKEN }} with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Add GitLab public keys
run: | run: |
git config --global credential.helper store ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
echo "https://user:${HF_TOKEN}@huggingface.co" > ~/.git-credentials
- name: Ensure branch - name: Ensure branch
run: | run: |
@@ -68,7 +70,6 @@ jobs:
git config lfs.activitytimeout 300 git config lfs.activitytimeout 300
git config lfs.transfer.maxretries 5 git config lfs.transfer.maxretries 5
git config lfs.concurrenttransfers 4 git config lfs.concurrenttransfers 4
git config lfs.allowincompletepush true
- name: Push LFS - name: Push LFS
id: sync-and-commit id: sync-and-commit
@@ -82,80 +83,3 @@ jobs:
done done
echo "All push attempts failed" echo "All push attempts failed"
exit 1 exit 1
- name: Checkout sunnypilot
uses: actions/checkout@v4
with:
repository: 'sunnypilot/sunnypilot'
path: sunnypilot
fetch-depth: 0
filter: blob:limit=1k
lfs: false
- name: Reference objects on sunnypilot-lfs
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
set -euo pipefail
GIT_LFS_SKIP_SMUDGE=1 git clone --quiet https://huggingface.co/sunnypilot/sunnypilot-lfs hf-lfs
mkdir -p hf-lfs/all
cat > scan_pointers.py <<'EOF'
import re, subprocess, sys, threading
repo = sys.argv[1]
chk = subprocess.run(["git", "-C", repo, "cat-file", "--batch-all-objects",
"--batch-check=%(objectname) %(objecttype) %(objectsize)"],
capture_output=True, text=True, check=True).stdout
small = [f[0] for f in (l.split() for l in chk.splitlines()) if f[1] == "blob" and int(f[2]) < 1024]
p = subprocess.Popen(["git", "-C", repo, "cat-file", "--batch=%(objectsize)"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def feed():
for sha in small:
p.stdin.write(sha.encode() + b"\n")
p.stdin.close()
threading.Thread(target=feed, daemon=True).start()
rx = re.compile(rb"^version https://git-lfs\.github\.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize ([0-9]+)\n")
seen = {}
for _ in small:
n = int(p.stdout.readline()); body = p.stdout.read(n); p.stdout.read(1)
m = rx.match(body)
if m:
seen.setdefault(m.group(1).decode(), m.group(2).decode())
for oid, size in sorted(seen.items()):
print(oid, size)
EOF
for repo in . sunnypilot; do
python3 scan_pointers.py "$repo"
done | sort -u | while read -r oid size; do
[ -e "hf-lfs/all/$oid" ] && continue
printf 'version https://git-lfs.github.com/spec/v1\noid sha256:%s\nsize %s\n' "$oid" "$size" > "hf-lfs/all/$oid"
echo "$oid"
done > new_oids.txt
echo "new pointer files: $(wc -l < new_oids.txt)"
dropped=0
while mapfile -t -n 100 chunk && ((${#chunk[@]})); do
body=$(for oid in "${chunk[@]}"; do
size=$(awk '/^size /{print $2}' "hf-lfs/all/$oid")
printf '{"oid":"%s","size":%s}\n' "$oid" "$size"
done | jq -s '{operation:"download",transfers:["basic"],objects:.}')
for attempt in 1 2 3 4 5; do
resp=$(curl -sS -u "user:${HF_TOKEN}" -w '\n%{http_code}' \
-H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' \
--data "$body" "${LFS_URL}/objects/batch") && [ "$(tail -n1 <<<"$resp")" = 200 ] && break
echo "batch check attempt $attempt failed ($(tail -n1 <<<"$resp")), retrying in 60s"; sleep 60
done
[ "$(tail -n1 <<<"$resp")" = 200 ] || { echo "batch check failed"; exit 1; }
for oid in $(sed '$d' <<<"$resp" | jq -r '.objects[] | select(.error) | .oid'); do
rm -f "hf-lfs/all/$oid"; dropped=$((dropped+1))
done
done < new_oids.txt
echo "dropped (not on HF): $dropped"
cd hf-lfs
git add -A all
if git diff --cached --quiet; then
echo "nothing new to reference"
exit 0
fi
git commit -q -m "Reference $(git diff --cached --name-only | wc -l | tr -d ' ') new LFS objects"
git push origin HEAD:main
+28
View File
@@ -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 }}
+1 -5
View File
@@ -7,7 +7,7 @@ on:
env: env:
GIT_CONFIG_COUNT: 1 GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs: jobs:
build___nightly: build___nightly:
@@ -34,7 +34,3 @@ jobs:
- run: ./tools/op.sh setup - run: ./tools/op.sh setup
- name: Push __nightly - name: Push __nightly
run: BRANCH=__nightly tools/release/build_stripped.sh run: BRANCH=__nightly tools/release/build_stripped.sh
- name: Push chestnut nightly
run: |
git lfs pull --exclude=''
INCLUDE_BIG_MODEL=1 BRANCH=__nightly-chestnut tools/release/build_stripped.sh
+1 -1
View File
@@ -11,7 +11,7 @@ env:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
GIT_CONFIG_COUNT: 1 GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs: jobs:
package_updates: package_updates:
+77 -115
View File
@@ -31,7 +31,7 @@ on:
type: string type: string
default: '' default: ''
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)' description: 'Hardware target to compile for (qcom or usbgpu)'
required: false required: false
type: string type: string
default: 'qcom' default: 'qcom'
@@ -57,7 +57,7 @@ on:
type: choice type: choice
options: options:
- qcom - qcom
- chestnut - usbgpu
default: 'qcom' default: 'qcom'
@@ -100,60 +100,28 @@ jobs:
commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y')
echo "model_date=${commit_date}" >> $GITHUB_OUTPUT echo "model_date=${commit_date}" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
- run: |
- name: Fetch Models
run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot/openpilot
fetch_lfs() { if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
sudo apt-get install -y -qq aria2 > /dev/null # Parallel ranges because gh is flaky git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
LFS_URL=$(git config -f "$(git rev-parse --show-toplevel)/.lfsconfig" lfs.url) rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
for f in "$@"; do
grep -qs "^version https://git-lfs" "$f" || continue
OID=$(awk -F'sha256:' '/^oid/{print $2}' "$f"); SIZE=$(awk '/^size/{print $2}' "$f")
HREF=$(curl -fsSL --retry 3 -X POST -H "Accept: application/vnd.git-lfs+json" -H "Content-Type: application/vnd.git-lfs+json" "$LFS_URL/objects/batch" \
-d "{\"operation\":\"download\",\"transfer\":[\"basic\"],\"objects\":[{\"oid\":\"$OID\",\"size\":$SIZE}]}" | jq -r '.objects[0].actions.download.href')
aria2c -x 16 -s 16 -k 16M --max-tries=5 --retry-wait=5 --file-allocation=none --summary-interval=30 --allow-overwrite=true \
-d "$(dirname "$f")" -o "$(basename "$f")" "$HREF"
echo "$OID $f" | sha256sum -c -
done
}
UPSTREAM_PKL=selfdrive/modeld/models/big_driving_tinygrad.pkl
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx,**/selfdrive/modeld/models/big_*.pkl,**/selfdrive/modeld/models/dmonitoring_*.pkl"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx selfdrive/modeld/models/big_*.pkl selfdrive/modeld/models/dmonitoring_*.pkl
elif compgen -G "selfdrive/modeld/models/big_*.onnx" > /dev/null; then
fetch_lfs selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/big_*.pkl
find selfdrive/modeld/models -type f \( -name "*.onnx" -o -name "*.pkl" \) ! -name "big_*.onnx" ! -name "big_*.pkl" -delete
elif [ -f "$UPSTREAM_PKL" ]; then
echo "No big model ONNX tracked: converting commaai's precompiled $UPSTREAM_PKL, with the tinygrad it was built with"
fetch_lfs "$UPSTREAM_PKL"
find selfdrive/modeld/models -type f \( -name "*.onnx" -o -name "*.pkl" \) ! -path "$UPSTREAM_PKL" -delete
GITMODULES="$(git rev-parse --show-toplevel)/.gitmodules"
TG_NAME=$(git config -f "$GITMODULES" --get-regexp '^submodule\..*\.path$' | awk '$2 == "tinygrad_repo" {sub(/^submodule\./, "", $1); sub(/\.path$/, "", $1); print $1}')
TG_URL=$(git config -f "$GITMODULES" "submodule.$TG_NAME.url" | sed 's|^\.\./\.\./|https://github.com/|')
echo "$TG_URL $(git rev-parse HEAD:tinygrad_repo) $(sha256sum "$UPSTREAM_PKL" | cut -d' ' -f1)" > selfdrive/modeld/models/upstream_pkl.txt
else else
echo "::error::no big model ONNX or precompiled big_driving_tinygrad.pkl at this ref" git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
exit 1 find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
fi fi
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx selfdrive/modeld/models/*.pkl 2>/dev/null; then echo "::error::the ONNX files above are still LFS pointers, not real models"
echo "::error::the ONNX or PKL files above are still LFS pointers, not real models"
exit 1 exit 1
fi fi
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }} name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
compression-level: 0 path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
path: |
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.pkl
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/upstream_pkl.txt
if-no-files-found: error if-no-files-found: error
build_model: build_model:
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"] runs-on: [self-hosted, usbgpu]
needs: get_model needs: get_model
env: env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
@@ -164,6 +132,7 @@ jobs:
fetch-depth: 1 fetch-depth: 1
submodules: recursive submodules: recursive
- name: Set environment variables - name: Set environment variables
id: set-env id: set-env
run: | run: |
@@ -175,6 +144,9 @@ jobs:
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync --frozen uv sync --frozen
printenv >> $GITHUB_ENV printenv >> $GITHUB_ENV
if [[ "${{ runner.debug }}" == "1" ]]; then
cat $GITHUB_OUTPUT
fi
- name: Setup build environment - name: Setup build environment
run: | run: |
@@ -186,6 +158,10 @@ jobs:
echo "VERSION: ${{ steps.set-env.outputs.version }}" echo "VERSION: ${{ steps.set-env.outputs.version }}"
echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}"
echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" echo "VIRTUAL_ENV: ${VIRTUAL_ENV}"
echo "-------"
if [[ "${{ runner.debug }}" == "1" ]]; then
printenv
fi
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
rm -rf ${{ env.MODELS_DIR }}/*.onnx* rm -rf ${{ env.MODELS_DIR }}/*.onnx*
@@ -208,11 +184,11 @@ jobs:
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") 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}')") 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 OPENPILOT_HACKS=1" TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "CHESTNUT build" echo "USBGPU build"
export CHESTNUT=1 export USBGPU=1
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32" 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" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else else
echo "QCOM build" echo "QCOM build"
@@ -220,77 +196,64 @@ jobs:
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
fi fi
NATIVE_PKL=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "*.pkl" -print -quit) # 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
if [ -f "${{ env.MODELS_DIR }}/upstream_pkl.txt" ]; then # Detect model type and build compile args
read -r TG_URL TG_SHA _ < "${{ env.MODELS_DIR }}/upstream_pkl.txt" VISION_ONNX=""
echo "Converting commaai's precompiled model with tinygrad $TG_SHA" for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
UPSTREAM_TG="${{ runner.temp }}/upstream_tinygrad" [ -f "$f" ] && VISION_ONNX="$f" && break
rm -rf "$UPSTREAM_TG" && git init -q "$UPSTREAM_TG" && git -C "$UPSTREAM_TG" fetch -q --depth 1 "$TG_URL" "$TG_SHA" && git -C "$UPSTREAM_TG" checkout -q FETCH_HEAD done
CONVERTED="${{ runner.temp }}/big_driving_tinygrad_plain.pkl"
env DEV=USB+AMD PYTHONPATH="$UPSTREAM_TG:${{ github.workspace }}" taskset -c 7 python3 openpilot/sunnypilot/modeld_v2/convert_upstream_pkl.py \
"${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" "$CONVERTED"
mv "$CONVERTED" "$OUTPUT_PKL"
elif NEW_ARCH_ONNX=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "${{ inputs.target_hardware == 'chestnut' && 'big_' || '' }}*driving*.onnx" -print -quit) &&
[ -n "$NEW_ARCH_ONNX" ] && grep -qa new_img "$NEW_ARCH_ONNX"; then # the new architecture: one ONNX taking the warped frames as new_img
echo "Compiling new architecture ONNX: $NEW_ARCH_ONNX"
env ${TG_FLAGS} taskset -c 7 python3 openpilot/sunnypilot/modeld_v2/compile_modeld_v2.py \
"$NEW_ARCH_ONNX" "$OUTPUT_PKL" --device-input "*" --benchmark-runs 1
else
echo "Detected onnx architecture. Routing to native compile_modeld.py."
# 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="" POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break [ -f "$f" ] && POLICY_ONNX="$f" && break
done done
OFF_POLICY_ONNX="" OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do 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 [ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done done
ON_POLICY_ONNX="" ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do 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 [ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done done
SUPERCOMBO_ONNX="" 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 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 [ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
done done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME="" MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
if [ -f "$VISION_ONNX" ]; then if [ -f "$VISION_ONNX" ]; then
ONNX_ARGS="--vision-onnx $VISION_ONNX" ONNX_ARGS="--vision-onnx $VISION_ONNX"
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX" 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 elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX" ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
elif [ -f "$POLICY_ONNX" ]; then elif [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_policy MODEL_TYPE=vision_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX" ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
fi
elif [ -f "$SUPERCOMBO_ONNX" ]; then
MODEL_TYPE=supercombo
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
fi fi
elif [ -f "$SUPERCOMBO_ONNX" ]; then
MODEL_TYPE=supercombo
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
fi
if [ -n "$MODEL_TYPE" ]; then if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL" echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} taskset -c 7 python3 "$COMPILE_MODELD" \ env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \ --model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \ --model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \ --camera-resolutions $CAMERA_RES \
$ONNX_ARGS \ $ONNX_ARGS \
--output "$OUTPUT_PKL" --output "$OUTPUT_PKL"
fi
fi fi
- name: Prepare Output - name: Prepare Output
@@ -301,7 +264,8 @@ jobs:
# Copy the model files # Copy the model files
rsync -avm \ rsync -avm \
--include='*.dlc' \ --include='*.dlc' \
--include='/${{ inputs.target_hardware == 'chestnut' && 'big_' || '' }}driving_tinygrad.pkl' \ --include='*.chunk*' \
--include='*.chunkmanifest' \
--exclude='*' \ --exclude='*' \
--delete-excluded \ --delete-excluded \
--chown=comma:comma \ --chown=comma:comma \
@@ -312,7 +276,6 @@ jobs:
--output-dir "${{ env.OUTPUT_DIR }}" \ --output-dir "${{ env.OUTPUT_DIR }}" \
--custom-name "${{ env.MODEL_NAME }}" \ --custom-name "${{ env.MODEL_NAME }}" \
--upstream-branch "${{ inputs.upstream_branch }}" \ --upstream-branch "${{ inputs.upstream_branch }}" \
$([ -f "${{ env.MODELS_DIR }}/upstream_pkl.txt" ] && echo "--onnx-sha256 $(cut -d' ' -f3 "${{ env.MODELS_DIR }}/upstream_pkl.txt")") \
${{ inputs.is_20hz && '--is-20hz' || '' }} ${{ inputs.is_20hz && '--is-20hz' || '' }}
- name: Write artifact name to file - name: Write artifact name to file
@@ -324,7 +287,6 @@ jobs:
with: with:
name: model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }} name: model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }}
path: ${{ env.OUTPUT_DIR }} path: ${{ env.OUTPUT_DIR }}
compression-level: 0
- name: Upload artifact name file - name: Upload artifact name file
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+49 -133
View File
@@ -170,9 +170,6 @@ jobs:
scons -j1 cache_dir="$SCONS_CACHE" --minimal \ scons -j1 cache_dir="$SCONS_CACHE" --minimal \
openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd
echo "Building rest of sunnypilot" echo "Building rest of sunnypilot"
sudo rm -rf /tmp/* || true
mkdir -p "$BUILD_DIR/tmp"
export TMPDIR="$BUILD_DIR/tmp"
SKIP_TINYGRAD_COMPILE=1 /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal SKIP_TINYGRAD_COMPILE=1 /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal
touch ${BUILD_DIR}/prebuilt touch ${BUILD_DIR}/prebuilt
if [[ "${{ runner.debug }}" == "1" ]]; then if [[ "${{ runner.debug }}" == "1" ]]; then
@@ -219,79 +216,54 @@ jobs:
needs: [ prepare_strategy ] needs: [ prepare_strategy ]
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }} if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
concurrency:
group: prepare-chestnut
cancel-in-progress: false
outputs: outputs:
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }} onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
env: env:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
HF_REPO: sunnypilot/sunnypilot_models_v1 HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/big HF_DEFAULTS_PATH: models/defaults/big
steps: steps:
- name: Resolve tinygrad ref via API - name: Resolve ONNX hash and tinygrad ref via API
id: resolve id: resolve
run: | run: |
REF="${{ github.head_ref || github.ref_name }}" REF="${{ github.head_ref || github.ref_name }}"
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl?ref=${REF}" --jq '.sha') 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)
[ -n "$BLOB_SHA" ] || { echo "::error::Failed to resolve big_driving_tinygrad.pkl blob SHA"; exit 1; } echo "ONNX hash: $ONNX_HASH"
ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
echo "big PKL hash: $ONNX_HASH"
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract big PKL hash"; exit 1; }
echo "onnx_sha256=$ONNX_HASH" >> "$GITHUB_OUTPUT"
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF" echo "tinygrad ref: $TINYGRAD_REF"
echo "tinygrad_ref=$TINYGRAD_REF" >> "$GITHUB_OUTPUT"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() { check_defaults() {
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1 DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" --arg ref "$TINYGRAD_REF" ' TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $ref or (.tinygrad_ref == null and $root.tinygrad_ref == $ref)))] | .[0] // empty [ "$TINYGRAD_MATCH" = "true" ] || return 1
' 2>/dev/null) BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
[ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ] [ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ]
} }
if check_defaults; then if check_defaults; then
echo "HF defaults match repo tinygrad ref" echo "HF defaults match repo ONNX hash and tinygrad ref"
exit 0 exit 0
fi fi
echo "No matching model on HF — dispatching build" echo "No matching model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=big gh workflow run build-default-models.yaml --ref "$REF" -f target=big
sleep 10
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId') echo "Polling HF for big model availability..."
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 90); do for i in $(seq 1 90); do
sleep 30 sleep 30
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status') if check_defaults; then
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion') echo "Big model available on HF after $((i * 30))s"
echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION" exit 0
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "Big model verified on HF"
exit 0
fi
echo "::error::Build succeeded but model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi fi
echo "Poll $i/90: not yet available"
done done
echo "::error::Build run did not complete within 45 minutes" echo "::error::Big model not available on HF after 45 minutes"
exit 1 exit 1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -305,78 +277,54 @@ jobs:
prepare_small_model: prepare_small_model:
needs: [ prepare_strategy ] needs: [ prepare_strategy ]
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
concurrency:
group: prepare-small-model
cancel-in-progress: false
outputs: outputs:
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }} driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
env: env:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
HF_REPO: sunnypilot/sunnypilot_models_v1 HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/small HF_DEFAULTS_PATH: models/defaults/small
steps: steps:
- name: Resolve tinygrad ref via API - name: Resolve ONNX hash and tinygrad ref via API
id: resolve id: resolve
run: | run: |
REF="${{ github.head_ref || github.ref_name }}" REF="${{ github.head_ref || github.ref_name }}"
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.sha') 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)
DRIVING_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "Driving ONNX hash: $DRIVING_HASH" echo "Driving ONNX hash: $DRIVING_HASH"
[ -n "$DRIVING_HASH" ] || { echo "::error::Failed to extract driving ONNX hash"; exit 1; } echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
echo "driving_onnx_sha256=$DRIVING_HASH" >> "$GITHUB_OUTPUT"
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF" echo "tinygrad ref: $TINYGRAD_REF"
echo "tinygrad_ref=$TINYGRAD_REF" >> "$GITHUB_OUTPUT"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() { check_defaults() {
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1 DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" --arg ref "$TINYGRAD_REF" ' TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $ref or (.tinygrad_ref == null and $root.tinygrad_ref == $ref)))] | .[0] // empty [ "$TINYGRAD_MATCH" = "true" ] || return 1
' 2>/dev/null) DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
[ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] [ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] || return 1
} }
if check_defaults; then if check_defaults; then
echo "HF defaults match repo tinygrad ref" echo "HF defaults match repo ONNX hash and tinygrad ref"
exit 0 exit 0
fi fi
echo "No matching model on HF — dispatching build" echo "No matching model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=small gh workflow run build-default-models.yaml --ref "$REF" -f target=small
sleep 10
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId') echo "Polling HF for model availability..."
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 60); do for i in $(seq 1 60); do
sleep 30 sleep 30
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status') if check_defaults; then
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion') echo "Model available on HF after $((i * 30))s"
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION" exit 0
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "Small model verified on HF"
exit 0
fi
echo "::error::Build succeeded but model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi fi
echo "Poll $i/60: not yet available"
done done
echo "::error::Small model build did not complete within 30 minutes" echo "::error::Small driving model not available on HF after 30 minutes"
exit 1 exit 1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -390,78 +338,54 @@ jobs:
prepare_dm_model: prepare_dm_model:
needs: [ prepare_strategy ] needs: [ prepare_strategy ]
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
concurrency:
group: prepare-dm-model
cancel-in-progress: false
outputs: outputs:
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
env: env:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
HF_REPO: sunnypilot/sunnypilot_models_v1 HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/dm HF_DEFAULTS_PATH: models/defaults/dm
steps: steps:
- name: Resolve tinygrad ref via API - name: Resolve ONNX hash and tinygrad ref via API
id: resolve id: resolve
run: | run: |
REF="${{ github.head_ref || github.ref_name }}" REF="${{ github.head_ref || github.ref_name }}"
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.sha') 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)
DM_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "DM ONNX hash: $DM_HASH" echo "DM ONNX hash: $DM_HASH"
[ -n "$DM_HASH" ] || { echo "::error::Failed to extract DM ONNX hash"; exit 1; } echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
echo "dm_onnx_sha256=$DM_HASH" >> "$GITHUB_OUTPUT"
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF" echo "tinygrad ref: $TINYGRAD_REF"
echo "tinygrad_ref=$TINYGRAD_REF" >> "$GITHUB_OUTPUT"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() { check_defaults() {
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1 DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" --arg ref "$TINYGRAD_REF" ' TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $ref or (.tinygrad_ref == null and $root.tinygrad_ref == $ref)))] | .[0] // empty [ "$TINYGRAD_MATCH" = "true" ] || return 1
' 2>/dev/null) DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
[ -n "$DM" ] && [ "$DM" != "null" ] [ -n "$DM" ] && [ "$DM" != "null" ] || return 1
} }
if check_defaults; then if check_defaults; then
echo "HF defaults match DM tinygrad ref" echo "HF defaults match DM ONNX hash and tinygrad ref"
exit 0 exit 0
fi fi
echo "No matching DM model on HF — dispatching build" echo "No matching DM model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
sleep 10
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId') echo "Polling HF for DM model availability..."
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 60); do for i in $(seq 1 60); do
sleep 30 sleep 30
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status') if check_defaults; then
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion') echo "DM model available on HF after $((i * 30))s"
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION" exit 0
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "DM model verified on HF"
exit 0
fi
echo "::error::Build succeeded but DM model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi fi
echo "Poll $i/60: not yet available"
done done
echo "::error::DM model build did not complete within 30 minutes" echo "::error::DM model not available on HF after 30 minutes"
exit 1 exit 1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -503,12 +427,11 @@ jobs:
mkdir -p ${{ env.OUTPUT_DIR }} mkdir -p ${{ env.OUTPUT_DIR }}
tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }}
- name: Download models from HF - name: Download model chunks from HF
uses: ./.github/workflows/download-hf-model uses: ./.github/workflows/download-hf-model-chunks
with: with:
hf_repo: sunnypilot/sunnypilot_models_v1 hf_repo: sunnypilot/sunnypilot_models_v1
dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models
tinygrad_ref: ${{ needs.prepare_dm_model.outputs.tinygrad_ref }}
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/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"},
@@ -573,12 +496,11 @@ jobs:
mkdir -p ${{ env.OUTPUT_DIR }} mkdir -p ${{ env.OUTPUT_DIR }}
tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }}
- name: Download models from HF - name: Download model chunks from HF
uses: ./.github/workflows/download-hf-model uses: ./.github/workflows/download-hf-model-chunks
with: with:
hf_repo: sunnypilot/sunnypilot_models_v1 hf_repo: sunnypilot/sunnypilot_models_v1
dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models
tinygrad_ref: ${{ needs.prepare_dm_model.outputs.tinygrad_ref }}
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/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"},
@@ -594,14 +516,8 @@ jobs:
- name: Publish chestnut branch - name: Publish chestnut branch
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: | run: |
CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut" CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut"
cp .lfsconfig "${{ env.OUTPUT_DIR }}/"
echo 'openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text' > "${{ env.OUTPUT_DIR }}/.gitattributes"
git lfs install
git config --global credential.helper store
echo "https://user:${HF_TOKEN}@huggingface.co" > ~/.git-credentials
${{ env.CI_DIR }}/publish.sh \ ${{ env.CI_DIR }}/publish.sh \
"${{ github.workspace }}" \ "${{ github.workspace }}" \
+78
View File
@@ -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"
-122
View File
@@ -1,122 +0,0 @@
name: Test Models Compatibility With Tinygrad Changes
on:
pull_request:
paths:
- 'tinygrad_repo'
- 'openpilot/sunnypilot/modeld_v2/**'
workflow_dispatch:
env:
CI: 1
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
plain-artifact:
name: Plain artifact round trip and pickle converter
runs-on: ubuntu-24.04
env:
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
TINYGRAD_TESTS: 1
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build openpilot
run: scons -j$(nproc)
- name: Compile, load, cache and fuse a model with the pinned tinygrad
env:
DEV: CPU
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_plain_artifact.py
- name: Install LLVM 20 and ROCm comgr for the amd mock, as tinygrad_repo/.github/actions/setup-tinygrad does
run: |
. /etc/os-release
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
- name: Convert a pickled USB+AMD jit and replay it under tinygrad's amd mock
env:
DEV: MOCKUSB+AMD
CHECK_OOB: 0
GMMU: 0
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
- name: Where tinygrad moved, for the helper or failure point named above
if: failure() && github.event_name == 'pull_request'
run: |
git fetch -q --depth=1 origin "${{ github.base_ref }}"
old=$(git rev-parse "origin/${{ github.base_ref }}:tinygrad_repo") new=$(git rev-parse HEAD:tinygrad_repo)
git -C tinygrad_repo fetch -q origin "$old" || true
echo "tinygrad_repo $old -> $new, in what helpers.py and plain_artifact_compiler.py read:"
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/uop/ops.py tinygrad/engine/jit.py tinygrad/engine/realize.py tinygrad/device.py \
tinygrad/helpers.py tinygrad/dtype.py tinygrad/renderer/__init__.py
echo "and in what convert_upstream_pkl.py decodes:"
git -C tinygrad_repo diff --stat "$old" "$new" -- tinygrad/runtime/autogen/am/pm4_nv.py tinygrad/runtime/autogen/amd_gpu.py
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/runtime/support/hcq2.py tinygrad/runtime/support/usb.py tinygrad/runtime/ops_amd.py
generate-matrix:
runs-on: ubuntu-latest
outputs:
models: ${{ steps.set-matrix.outputs.models }}
steps:
- uses: actions/checkout@v4
- name: Fetch and Parse json
id: set-matrix
run: |
python3 -c '
import json, urllib.request, os, re
with open("openpilot/sunnypilot/models/fetcher.py", "r") as f:
urls = re.findall(r"MODEL_URL(?:_CHESTNUT)?\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", f.read())
artifacts = []
for url in urls:
data = json.loads(urllib.request.urlopen(url).read())
for bundle in data.get("bundles", []):
for model in bundle.get("models", []):
if "artifact" in model:
artifacts.append(model["artifact"])
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"models={json.dumps(artifacts)}\n")
'
test-model:
name: Test ${{ matrix.artifact.file_name }}
needs: generate-matrix
runs-on: ubuntu-latest
container: ghcr.io/commaai/openpilot-base:latest
strategy:
fail-fast: false
matrix:
artifact: ${{ fromJson(needs.generate-matrix.outputs.models) }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Download model
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
MODEL_URL: ${{ matrix.artifact.download_uri.url }}
MODEL_FILE: ${{ matrix.artifact.file_name }}
shell: bash
run: |
mkdir -p /tmp/models
AUTH=()
if [ -n "$HF_TOKEN" ]; then AUTH=(-H "Authorization: Bearer $HF_TOKEN"); fi
curl --retry 3 --retry-all-errors -s -S -f -L "${AUTH[@]}" -o "/tmp/models/$MODEL_FILE" "$MODEL_URL"
- name: Run Model Compatibility Test
env:
MODEL_BASE_NAME: ${{ matrix.artifact.file_name }}
MODEL_DIR: "/tmp/models"
run: |
export PYTHONPATH="$GITHUB_WORKSPACE/tinygrad_repo:$GITHUB_WORKSPACE:$PYTHONPATH"
python3 -m pip uninstall -y tinygrad || uv pip uninstall -y tinygrad --system || true
python3 -m pytest openpilot/sunnypilot/modeld_v2/tests/test_models.py
+1 -1
View File
@@ -22,7 +22,7 @@ env:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
GIT_CONFIG_COUNT: 1 GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs: jobs:
build_release: build_release:
-2
View File
@@ -50,8 +50,6 @@ st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
*.stats *.stats
*.pkl *.pkl
*.pkl* *.pkl*
!openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
!openpilot/sunnypilot/modeld_v2/models/*.pkl
config.json config.json
compile_commands.json compile_commands.json
compare_runtime*.html compare_runtime*.html
-1
View File
@@ -19,4 +19,3 @@
[submodule "sunnypilot/neural_network_data"] [submodule "sunnypilot/neural_network_data"]
path = openpilot/sunnypilot/neural_network_data path = openpilot/sunnypilot/neural_network_data
url = https://github.com/sunnypilot/neural-network-data.git url = https://github.com/sunnypilot/neural-network-data.git
+2 -3
View File
@@ -1,5 +1,4 @@
[lfs] [lfs]
url = https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs
pushurl = https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git
locksverify = false locksverify = false
Vendored
+4 -62
View File
@@ -12,17 +12,16 @@ def retryWithDelay(int maxRetries, int delay, Closure body) {
def device(String ip, String step_label, String cmd) { def device(String ip, String step_label, String cmd) {
withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) { withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) {
def ssh_cmd = """ def ssh_cmd = """
ssh -o ControlMaster=no -o ControlPath=none -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=12 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec setpriv --pdeathsig HUP /usr/bin/bash <<'END' ssh -o ControlMaster=auto -o ControlPath=/tmp/ssh_control_%C -o ControlPersist=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END'
set -e set -e
export TERM=xterm-256color export TERM=xterm-256color
trap 'kill 0' HUP # stop this process group on SSH disconnect shopt -s huponexit # kill all child processes when the shell exits
export CI=1 export CI=1
export PYTHONWARNINGS=error export PYTHONWARNINGS=error
export PYTHONFAULTHANDLER=1
export COMMA_CACHE=/data/tmp/comma_download_cache export COMMA_CACHE=/data/tmp/comma_download_cache
#export LOGPRINT=debug # this has gotten too spammy... #export LOGPRINT=debug # this has gotten too spammy...
export TEST_DIR=${env.TEST_DIR} export TEST_DIR=${env.TEST_DIR}
@@ -70,8 +69,7 @@ export LD_LIBRARY_PATH="\$(python -c 'import ffmpeg; print(ffmpeg.LIB_DIR)'):/us
ln -snf ${env.TEST_DIR} /data/pythonpath ln -snf ${env.TEST_DIR} /data/pythonpath
cd ${env.TEST_DIR} || true cd ${env.TEST_DIR} || true
time ( ${cmd} ) & time ${cmd}
wait \$!
END""" END"""
sh script: ssh_cmd, label: step_label sh script: ssh_cmd, label: step_label
@@ -161,14 +159,6 @@ def step(String name, String cmd, Map args = [:]) {
return [name, cmd, args] return [name, cmd, args]
} }
def chestnutStage(String name = "chestnut") {
deviceStage(name, "mici-chestnut-ci", ["UNSAFE=1", "CHESTNUT=1"], [
step("build", "./openpilot/selfdrive/test/chestnut.sh"),
step("model replay", "openpilot/selfdrive/test/process_replay/model_replay.py --chestnut"),
step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py TestChestnutOnroad", [timeout: 120]),
])
}
node { node {
env.CI = "1" env.CI = "1"
env.PYTHONWARNINGS = "error" env.PYTHONWARNINGS = "error"
@@ -179,37 +169,7 @@ node {
env.GIT_BRANCH = checkout(scm).GIT_BRANCH env.GIT_BRANCH = checkout(scm).GIT_BRANCH
env.GIT_COMMIT = checkout(scm).GIT_COMMIT env.GIT_COMMIT = checkout(scm).GIT_COMMIT
if (env.JOB_BASE_NAME == 'chestnut-stresstest') { def excludeBranches = ['__nightly', 'devel', 'devel-staging',
sh 'git fetch --no-tags origin nightly-chestnut'
env.GIT_COMMIT = sh(script: 'git show FETCH_HEAD:git_src_commit', returnStdout: true).trim()
if (!(env.GIT_COMMIT ==~ /[0-9a-f]{40}/)) {
error("invalid nightly-chestnut source commit")
}
def results = []
try {
timeout(time: 12, unit: 'HOURS') {
for (int run = 1; run <= 100; run++) {
results << "run ${run}: INCOMPLETE"
try {
chestnutStage("chestnut ${run}/100")
results[run - 1] = "run ${run}: PASS"
} catch (hudson.AbortException e) {
results[run - 1] = "run ${run}: FAIL"
echo e.toString()
}
}
}
} finally {
writeFile file: 'chestnut_stress_report.txt', text: "commit: ${env.GIT_COMMIT}\ncompleted: ${results.count { !it.endsWith('INCOMPLETE') }}/100\n" + results.join('\n') + '\n'
archiveArtifacts artifacts: 'chestnut_stress_report.txt'
}
if (results.any { !it.endsWith('PASS') }) {
error("chestnut stress test failed")
}
return
}
def excludeBranches = ['__nightly', '__nightly-chestnut', 'devel', 'devel-staging',
'release-tizi', 'release-tizi-staging', 'release-mici', 'release-mici-staging', 'testing-closet*', 'hotfix-*'] 'release-tizi', 'release-tizi-staging', 'release-mici', 'release-mici-staging', 'testing-closet*', 'hotfix-*']
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*') def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
@@ -241,21 +201,6 @@ node {
) )
} }
if (env.BRANCH_NAME == '__nightly-chestnut') {
parallel (
'nightly-chestnut': {
deviceStage("build nightly-chestnut", "mici-chestnut-ci", ["CHESTNUT=1"], [
step("build nightly-chestnut", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 RELEASE_BRANCH=nightly-chestnut $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"),
])
},
'nightly-chestnut-dev': {
deviceStage("build nightly-chestnut-dev", "mici-chestnut-ci", ["CHESTNUT=1"], [
step("build nightly-chestnut-dev", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-chestnut-dev $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"),
])
},
)
}
if (!env.BRANCH_NAME.matches(excludeRegex)) { if (!env.BRANCH_NAME.matches(excludeRegex)) {
parallel ( parallel (
'onroad tests': { 'onroad tests': {
@@ -307,9 +252,6 @@ node {
step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"), step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"),
]) ])
}, },
'chestnut': {
chestnutStage()
},
) )
} }
+1 -11
View File
@@ -1,14 +1,4 @@
Version 0.11.3 (2026-XX-XX) Version 0.11.2 (2026-08-12)
========================
* Autospeed
* On supported cars, openpilot will intelligently decide what speed to drive based on the scene, including:
* stopping at red lights, and going at green lights
* slowing down for curves
* adjusting speed based on surrounding traffic
* This behavior was previously available with openpilot's "Experimental mode", but is now out of beta and always enabled
* With openpilot deciding which speed to drive, the car's cruise speed is treated as a maximum speed openpilot will not exceed
Version 0.11.2 (2026-10-14)
======================= =======================
* New driving model * New driving model
* Big model with 880M parameters * Big model with 880M parameters
+13 -2
View File
@@ -20,7 +20,7 @@ SetOption('num_jobs', max(1, int(os.cpu_count()/(1 if "CI" in os.environ else 2)
AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line')
AddOption('--verbose', action='store_true', default=False, help='show full build commands') AddOption('--verbose', action='store_true', default=False, help='show full build commands')
release = not os.path.exists(File('#.gitmodules').abspath) # file absent on release branch, see release_files.py release = not os.path.exists(File('#.gitattributes').abspath) # file absent on release branch, see release_files.py
AddOption('--minimal', AddOption('--minimal',
action='store_false', action='store_false',
dest='extras', dest='extras',
@@ -87,6 +87,7 @@ acados_include_dirs = [
# vendored in commaai/dependencies. # vendored in commaai/dependencies.
allowed_system_libs = { allowed_system_libs = {
"EGL", "GLESv2", "GL", "EGL", "GLESv2", "GL",
"Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
"dl", "drm", "gbm", "m", "pthread", "dl", "drm", "gbm", "m", "pthread",
} }
@@ -120,7 +121,6 @@ def _libflags(target, source, env, for_signature):
env = Environment( env = Environment(
ENV={ ENV={
"PATH": os.environ['PATH'], "PATH": os.environ['PATH'],
"TMPDIR": os.environ.get('TMPDIR', '/tmp'),
"PYTHONPATH": os.pathsep.join(submodule_python_paths), "PYTHONPATH": os.pathsep.join(submodule_python_paths),
"ACADOS_SOURCE_DIR": acados.DIR, "ACADOS_SOURCE_DIR": acados.DIR,
"ACADOS_PYTHON_INTERFACE_PATH": acados.TEMPLATE_DIR, "ACADOS_PYTHON_INTERFACE_PATH": acados.TEMPLATE_DIR,
@@ -316,6 +316,8 @@ def count_scons_nodes(nodes):
if node in seen: if node in seen:
continue continue
seen.add(node) seen.add(node)
if hasattr(node, 'has_builder') and node.has_builder():
build_product_nodes.add(node)
executor = node.get_executor() executor = node.get_executor()
if executor is not None: if executor is not None:
stack += executor.get_all_prerequisites() + executor.get_all_children() stack += executor.get_all_prerequisites() + executor.get_all_children()
@@ -324,6 +326,7 @@ def count_scons_nodes(nodes):
progress_interval = 5 progress_interval = 5
progress_count = 0 progress_count = 0
build_product_nodes = set()
progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))) progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry)))
def progress_function(node): def progress_function(node):
@@ -339,3 +342,11 @@ def progress_function(node):
Progress(progress_function, interval=progress_interval) Progress(progress_function, interval=progress_interval)
AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir) AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir)
def check_build_product_size(target, source, env):
limit = 50 * 1024 * 1024 # GitHub max size
for t in target:
if hasattr(t, 'isfile') and t.isfile() and (size := os.path.getsize(t.abspath)) > limit:
raise SCons.Errors.UserError(f"{t} is {size / (1024 * 1024):.1f} MiB, exceeding the {limit / (1024 * 1024):.1f} MiB limit")
if not GetOption('extras'):
AddPostAction(list(build_product_nodes), Action(check_build_product_size, None))
+7 -7
View File
@@ -34,7 +34,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>||| |Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>|||
|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>||| |CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|CUPRA|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>||| |CUPRA[<sup>12</sup>](#footnotes)|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>|||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>||| |Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>||| |Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>||| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
@@ -99,7 +99,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Fit 2018-20">Buy Here</a></sub></details>||| |Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Freed 2020">Buy Here</a></sub></details>||| |Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Freed 2020">Buy Here</a></sub></details>|||
|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2019-22">Buy Here</a></sub></details>||| |Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2019-22">Buy Here</a></sub></details>|||
|Honda|HR-V 2023-27|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2023-27">Buy Here</a></sub></details>||| |Honda|HR-V 2023-25|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2023-25">Buy Here</a></sub></details>|||
|Honda|Insight 2019-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Insight 2019-22">Buy Here</a></sub></details>||| |Honda|Insight 2019-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Insight 2019-22">Buy Here</a></sub></details>|||
|Honda|Inspire 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Inspire 2018">Buy Here</a></sub></details>||| |Honda|Inspire 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Inspire 2018">Buy Here</a></sub></details>|||
|Honda|N-Box 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda N-Box 2018">Buy Here</a></sub></details>||| |Honda|N-Box 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda N-Box 2018">Buy Here</a></sub></details>|||
@@ -268,8 +268,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>||| |Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>||| |Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>||| |Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-24[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-24">Buy Here</a></sub></details>||| |Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2023-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2023-25">Buy Here</a></sub></details>||| |Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard 2019-20">Buy Here</a></sub></details>||| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard 2019-20">Buy Here</a></sub></details>|||
|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard Hybrid 2021">Buy Here</a></sub></details>||| |Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>||| |Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>|||
@@ -335,8 +335,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Volkswagen[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Volkswagen[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>|||
|Volkswagen|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|Volkswagen|Passat 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>||| |Volkswagen|Passat 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
@@ -363,7 +363,7 @@ A supported vehicle is one that just works when you install a comma device. All
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br /> <sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br /> <sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br /> <sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
<sup>9</sup>Model years 2023 and 2024 can have either hardware type, depending on build date and factory. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br /> <sup>9</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br /> <sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
<sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br /> <sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
<sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br /> <sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
+2 -2
View File
@@ -5,10 +5,10 @@ The site is updated on pushes to master by this [workflow](../.github/workflows/
**1. Build the site** **1. Build the site**
``` bash ``` bash
op docs --build python docs/serve.py --build
``` ```
**2. Run the site locally** (rebuilds on change) **2. Run the site locally** (rebuilds on change)
``` bash ``` bash
op docs python docs/serve.py
``` ```
+10 -12
View File
@@ -18,21 +18,21 @@ function agnos_init {
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
# Check if AGNOS update is required # Check if AGNOS update is required
if [ "$(< /VERSION)" != "$AGNOS_VERSION" ]; then if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
AGNOS_PY="$DIR/openpilot/common/hardware/comma/agnos.py" AGNOS_PY="$DIR/openpilot/common/hardware/comma/agnos.py"
MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json" MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json"
if "$AGNOS_PY" --verify "$MANIFEST"; then if $AGNOS_PY --verify $MANIFEST; then
sudo reboot sudo reboot
fi fi
while true; do while true; do
"$DIR/openpilot/common/hardware/comma/updater" "$AGNOS_PY" "$MANIFEST" $DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
done done
fi fi
} }
function launch { function launch {
# Remove orphaned git lock if it exists on boot # Remove orphaned git lock if it exists on boot
[ -f "$DIR/.git/index.lock" ] && rm -f "$DIR/.git/index.lock" [ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock
# Check to see if there's a valid overlay-based update available. Conditions # Check to see if there's a valid overlay-based update available. Conditions
# are as follows: # are as follows:
@@ -44,7 +44,7 @@ function launch {
# that completed successfully and synced to disk. # that completed successfully and synced to disk.
if [ -f "${DIR}/.overlay_init" ]; then if [ -f "${DIR}/.overlay_init" ]; then
find "${DIR}/.git" -newer "${DIR}/.overlay_init" | grep -q '.' 2> /dev/null find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "${DIR} has been modified, skipping overlay update installation" echo "${DIR} has been modified, skipping overlay update installation"
else else
@@ -53,11 +53,9 @@ function launch {
echo "Valid overlay update found, installing" echo "Valid overlay update found, installing"
LAUNCHER_LOCATION="${BASH_SOURCE[0]}" LAUNCHER_LOCATION="${BASH_SOURCE[0]}"
mv "$DIR" /data/safe_staging/old_openpilot mv $DIR /data/safe_staging/old_openpilot
mv "${STAGING_ROOT}/finalized" "$DIR" mv "${STAGING_ROOT}/finalized" $DIR
cd "$DIR" cd $DIR
# releases before the openpilot/ move can't swap AGNOS on newer AGNOS, use ours
[ ! -L system/hardware/tici/agnos.json ] && /data/safe_staging/old_openpilot/openpilot/common/hardware/comma/agnos.py --verify system/hardware/tici/agnos.json && sudo reboot
echo "Restarting launch script ${LAUNCHER_LOCATION}" echo "Restarting launch script ${LAUNCHER_LOCATION}"
unset AGNOS_VERSION unset AGNOS_VERSION
@@ -71,7 +69,7 @@ function launch {
fi fi
# handle pythonpath # handle pythonpath
ln -sfn "$(pwd)" /data/pythonpath ln -sfn $(pwd) /data/pythonpath
export PYTHONPATH="$PWD" export PYTHONPATH="$PWD"
# submodule package symlinks for PYTHONPATH imports on device. # submodule package symlinks for PYTHONPATH imports on device.
@@ -92,7 +90,7 @@ function launch {
# start manager # start manager
cd openpilot/system/manager cd openpilot/system/manager
if [ ! -f "$DIR/prebuilt" ]; then if [ ! -f $DIR/prebuilt ]; then
./build.py ./build.py
fi fi
./manager.py ./manager.py
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12 export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.9" export AGNOS_VERSION="19.6"
fi fi
export STAGING_ROOT="/data/safe_staging" export STAGING_ROOT="/data/safe_staging"
+3 -10
View File
@@ -138,9 +138,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
status @0 :DownloadStatus; status @0 :DownloadStatus;
progress @1 :Float32; progress @1 :Float32;
eta @2 :UInt32; eta @2 :UInt32;
speed @3 :Float32;
canceling @4 :Bool;
resuming @5 :Bool;
} }
struct Chunk { struct Chunk {
@@ -199,6 +196,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
} }
struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
dec @0 :DynamicExperimentalControl;
longitudinalPlanSource @1 :LongitudinalPlanSource; longitudinalPlanSource @1 :LongitudinalPlanSource;
smartCruiseControl @2 :SmartCruiseControl; smartCruiseControl @2 :SmartCruiseControl;
speedLimit @3 :SpeedLimit; speedLimit @3 :SpeedLimit;
@@ -207,11 +205,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
events @6 :List(OnroadEventSP.Event); events @6 :List(OnroadEventSP.Event);
e2eAlerts @7 :E2eAlerts; e2eAlerts @7 :E2eAlerts;
deprecated :group { struct DynamicExperimentalControl {
dec @0 :DynamicExperimentalControlDEPRECATED;
}
struct DynamicExperimentalControlDEPRECATED {
state @0 :DynamicExperimentalControlState; state @0 :DynamicExperimentalControlState;
enabled @1 :Bool; enabled @1 :Bool;
active @2 :Bool; active @2 :Bool;
@@ -348,7 +342,7 @@ struct OnroadEventSP @0xda96579883444c35 {
silentParkBrake @11; silentParkBrake @11;
controlsMismatchLateral @12; controlsMismatchLateral @12;
hyundaiRadarTracksConfirmed @13; hyundaiRadarTracksConfirmed @13;
experimentalModeSwitchedDEPRECATED @14; experimentalModeSwitched @14;
wrongCarModeAlertOnly @15; wrongCarModeAlertOnly @15;
pedalPressedAlertOnly @16; pedalPressedAlertOnly @16;
laneTurnLeft @17; laneTurnLeft @17;
@@ -359,7 +353,6 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitPending @22; speedLimitPending @22;
e2eChime @23; e2eChime @23;
laneChangeRoadEdge @24; laneChangeRoadEdge @24;
bigModelReady @25;
} }
} }
+16 -24
View File
@@ -128,7 +128,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
personalityChanged @91; personalityChanged @91;
aeb @92; aeb @92;
userBookmark @95; userBookmark @95;
userBookmarkNotPaired @104;
excessiveActuation @96; excessiveActuation @96;
bigModelLoading @100; bigModelLoading @100;
bigModelFailed @102; bigModelFailed @102;
@@ -167,7 +166,12 @@ struct InitData {
gitSrcCommit @23 :Text; gitSrcCommit @23 :Text;
gitSrcCommitDate @24 :Text; gitSrcCommitDate @24 :Text;
androidProperties @16 :Map(Text, Text);
pandaInfo @8 :PandaInfo;
dirty @9 :Bool; dirty @9 :Bool;
passive @12 :Bool;
params @17 :Map(Text, Data); params @17 :Map(Text, Data);
commands @19 :Map(Text, Data); commands @19 :Map(Text, Data);
@@ -185,17 +189,6 @@ struct InitData {
mici @7; # comma four mici @7; # comma four
} }
deprecated :group {
gctx @1 :Text;
androidBuildInfo @5 :Deprecated.AndroidBuildInfo;
androidSensors @6 :List(Deprecated.AndroidSensor);
chffrAndroidExtra @7 :ChffrAndroidExtra;
pandaInfo @8 :PandaInfo;
passive @12 :Bool;
iosBuildInfo @14 :Deprecated.IosBuildInfo;
androidProperties @16 :Map(Text, Text);
}
struct PandaInfo { struct PandaInfo {
hasPanda @0 :Bool; hasPanda @0 :Bool;
dongleId @1 :Text; dongleId @1 :Text;
@@ -206,6 +199,14 @@ struct InitData {
struct ChffrAndroidExtra { struct ChffrAndroidExtra {
allCameraCharacteristics @0 :Map(Text, Text); allCameraCharacteristics @0 :Map(Text, Text);
} }
deprecated :group {
gctx @1 :Text;
androidBuildInfo @5 :Deprecated.AndroidBuildInfo;
androidSensors @6 :List(Deprecated.AndroidSensor);
chffrAndroidExtra @7 :ChffrAndroidExtra;
iosBuildInfo @14 :Deprecated.IosBuildInfo;
}
} }
struct FrameData { struct FrameData {
@@ -571,9 +572,6 @@ struct PandaState @0xa7649e2575e4591e {
controlsAllowedLateral @38 :Bool; controlsAllowedLateral @38 :Bool;
controlsAllowedLongitudinal @39 :Bool; controlsAllowedLongitudinal @39 :Bool;
nmiReset @40 :Bool;
hardfaultReset @41 :Bool;
enum FaultStatus { enum FaultStatus {
none @0; none @0;
faultTemp @1; faultTemp @1;
@@ -696,7 +694,6 @@ struct PeripheralState {
struct UsbState { struct UsbState {
devices @0 :List(Device); devices @0 :List(Device);
connected @1 :Bool;
struct Device { struct Device {
busnum @0 :UInt8; busnum @0 :UInt8;
@@ -728,7 +725,6 @@ struct ChestnutState {
pcieLtssm @7 :UInt8; pcieLtssm @7 :UInt8;
supplyVoltage @8 :UInt16; # mV supplyVoltage @8 :UInt16; # mV
supplyCurrent @9 :Int16; # mA supplyCurrent @9 :Int16; # mA
supplyFault @10 :Bool;
} }
struct RadarState @0x9a185389d6fdd05f { struct RadarState @0x9a185389d6fdd05f {
@@ -822,6 +818,7 @@ struct SelfdriveState {
alertHudVisual @12 :Car.CarControl.HUDControl.VisualAlert; alertHudVisual @12 :Car.CarControl.HUDControl.VisualAlert;
# configurable driving settings # configurable driving settings
experimentalMode @10 :Bool;
personality @11 :LongitudinalPersonality; personality @11 :LongitudinalPersonality;
enum AudibleAlert { enum AudibleAlert {
@@ -865,7 +862,6 @@ struct SelfdriveState {
deprecated :group { deprecated :group {
alertSound @8 :Car.CarControl.HUDControl.AudibleAlert; alertSound @8 :Car.CarControl.HUDControl.AudibleAlert;
experimentalMode @10 :Bool;
} }
} }
@@ -1008,7 +1004,6 @@ struct DrivingModelData {
frameIdExtra @1 :UInt32; frameIdExtra @1 :UInt32;
frameDropPerc @6 :Float32; frameDropPerc @6 :Float32;
modelExecutionTime @7 :Float32; modelExecutionTime @7 :Float32;
big @8 :Bool;
action @2 :ModelDataV2.Action; action @2 :ModelDataV2.Action;
@@ -1240,7 +1235,6 @@ struct LongitudinalPlan @0xe00b5b3eba12876c {
shouldStop @37: Bool; shouldStop @37: Bool;
allowThrottle @38: Bool; allowThrottle @38: Bool;
allowBrake @39: Bool; allowBrake @39: Bool;
accelBoost @40 :Float32;
solverExecutionTime @35 :Float32; solverExecutionTime @35 :Float32;
@@ -2238,7 +2232,6 @@ struct DriverMonitoringState {
pose @0: Bool; pose @0: Bool;
eye @1: Bool; eye @1: Bool;
phone @2: Bool; phone @2: Bool;
sleep @3: Bool;
} }
struct Pose { struct Pose {
@@ -2599,7 +2592,6 @@ struct Event {
clocks @35 :Clocks; clocks @35 :Clocks;
deviceState @6 :DeviceState; deviceState @6 :DeviceState;
chestnutState @152 :ChestnutState; chestnutState @152 :ChestnutState;
chestnutGpuState @153 :ChestnutState;
logMessage @18 :Text; logMessage @18 :Text;
errorLogMessage @85 :Text; errorLogMessage @85 :Text;
@@ -2710,8 +2702,8 @@ struct Event {
driverMonitoringStateDEPRECATED @71 :DriverMonitoringStateDEPRECATED; driverMonitoringStateDEPRECATED @71 :DriverMonitoringStateDEPRECATED;
gpsNMEADEPRECATED @3 :GPSNMEAData; gpsNMEADEPRECATED @3 :GPSNMEAData;
uploaderStateDEPRECATED @79 :UploaderState; uploaderStateDEPRECATED @79 :UploaderState;
navInstruction @82 :NavInstruction; navInstructionDEPRECATED @82 :NavInstruction;
navRoute @83 :NavRoute; navRouteDEPRECATED @83 :NavRoute;
navThumbnailDEPRECATED @84 :Thumbnail; navThumbnailDEPRECATED @84 :Thumbnail;
gnssMeasurementsDEPRECATED @91 :GnssMeasurements; gnssMeasurementsDEPRECATED @91 :GnssMeasurements;
mapRenderStateDEPRECATED @105: MapRenderState; mapRenderStateDEPRECATED @105: MapRenderState;
+1 -4
View File
@@ -25,8 +25,7 @@ _services: dict[str, tuple] = {
"accelerometer": (True, 104., 104), "accelerometer": (True, 104., 104),
"temperatureSensor": (True, 2., 200), "temperatureSensor": (True, 2., 200),
"deviceState": (True, 2., 1), "deviceState": (True, 2., 1),
"chestnutState": (True, 10., 1), "chestnutState": (True, 10., 10),
"chestnutGpuState": (False, 10.),
"touch": (True, 20., 1), "touch": (True, 20., 1),
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
"controlsState": (True, 100., 10, QueueSize.MEDIUM), "controlsState": (True, 100., 10, QueueSize.MEDIUM),
@@ -52,8 +51,6 @@ _services: dict[str, tuple] = {
"procLog": (True, 0.5, 15, QueueSize.BIG), "procLog": (True, 0.5, 15, QueueSize.BIG),
"gpsLocationExternal": (True, 10., 10), "gpsLocationExternal": (True, 10., 10),
"gpsLocation": (True, 1., 1), "gpsLocation": (True, 1., 1),
"navInstruction": (True, 1., 10),
"navRoute": (True, 0.),
"ubloxGnss": (True, 10.), "ubloxGnss": (True, 10.),
"qcomGnss": (True, 2.), "qcomGnss": (True, 2.),
"clocks": (True, 0.1, 1), "clocks": (True, 0.1, 1),
+1 -1
View File
@@ -1,4 +1,4 @@
from openpilot.common.api.comma_connect import API_HOST as API_HOST, CommaConnectApi from openpilot.common.api.comma_connect import CommaConnectApi
class Api: class Api:
-4
View File
@@ -16,10 +16,6 @@ class CV:
DEG_TO_RAD = np.pi / 180. DEG_TO_RAD = np.pi / 180.
RAD_TO_DEG = 1. / DEG_TO_RAD RAD_TO_DEG = 1. / DEG_TO_RAD
# Length
METER_TO_FOOT = 3.28084
METER_TO_MILE = 1. / 1609.344
# Mass # Mass
LB_TO_KG = 0.453592 LB_TO_KG = 0.453592
+1 -10
View File
@@ -1,8 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
WEBBING_ICCID_PREFIX = "8985235"
class LPAError(RuntimeError): class LPAError(RuntimeError):
pass pass
@@ -21,14 +19,7 @@ class Profile:
@property @property
def is_comma(self) -> bool: def is_comma(self) -> bool:
return self.provider == 'Webbing' and self.iccid.startswith(WEBBING_ICCID_PREFIX) return self.provider == 'Webbing' and self.iccid.startswith('8985235')
@property
def display_name(self) -> str:
if self.is_comma:
return "comma prime"
name = self.nickname or self.provider or "<unnamed>"
return f"{name} (...{self.iccid[-4:]})"
class LPABase(ABC): class LPABase(ABC):
+1 -1
View File
@@ -613,7 +613,7 @@ def parse_lpa_activation_code(activation_code: str) -> tuple[str, str]:
if not activation_code.startswith("LPA:"): if not activation_code.startswith("LPA:"):
raise ValueError("Invalid activation code format") raise ValueError("Invalid activation code format")
parts = activation_code[4:].split("$") parts = activation_code[4:].split("$")
if len(parts) != 3 or not all(parts): if len(parts) != 3:
raise ValueError("Invalid activation code format") raise ValueError("Invalid activation code format")
return parts[1], parts[2] return parts[1], parts[2]
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import io
import sys
import math
import os
from pathlib import Path
CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit
def get_chunk_name(name, idx, num_chunks):
return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}"
def get_manifest_path(name):
return f"{name}.chunkmanifest"
def _chunk_paths(path, num_chunks):
return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
def get_chunk_targets(path, file_size):
num_chunks = math.ceil(file_size / CHUNK_SIZE)
return _chunk_paths(path, num_chunks)
def chunk_file(path, targets):
manifest_path, *chunk_paths = targets
actual_num_chunks = max(1, math.ceil(os.path.getsize(path) / CHUNK_SIZE))
assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}"
with open(path, 'rb') as f:
for chunk_path in chunk_paths:
with open(chunk_path, 'wb') as out:
out.write(f.read(CHUNK_SIZE))
Path(manifest_path).write_text(str(len(chunk_paths)))
os.remove(path)
def get_existing_chunks(path):
if os.path.isfile(path):
return [path]
if os.path.isfile(manifest := get_manifest_path(path)):
num_chunks = int(Path(manifest).read_text().strip())
return _chunk_paths(path, num_chunks)
raise FileNotFoundError(path)
class ChunkStream(io.RawIOBase):
def __init__(self, paths):
self._paths = iter(paths)
self._f = None
def readable(self):
return True
def readinto(self, b):
n = 0
view = memoryview(b)
while n < len(b):
if self._f is None:
p = next(self._paths, None)
if p is None:
break
self._f = open(p, 'rb')
count = self._f.readinto(view[n:])
if not count:
self._f.close()
self._f = None
continue
n += count
return n
def open_file_chunked(path):
manifest_path = get_manifest_path(path)
if os.path.isfile(manifest_path):
num_chunks = int(Path(manifest_path).read_text().strip())
paths = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
elif os.path.isfile(path):
paths = [path]
else:
raise FileNotFoundError(path)
return io.BufferedReader(ChunkStream(paths))
if __name__ == "__main__":
path = sys.argv[1]
chunk_paths = get_chunk_targets(path, os.path.getsize(path))
chunk_file(path, chunk_paths)
+10
View File
@@ -27,6 +27,16 @@ def gpio_read(pin: int) -> bool | None:
return val return val
def gpio_export(pin: int) -> None:
if os.path.isdir(f"/sys/class/gpio/gpio{pin}"):
return
try:
with open("/sys/class/gpio/export", 'w') as f:
f.write(str(pin))
except Exception:
print(f"Failed to export gpio {pin}")
@cache @cache
def get_irq_action(irq: int) -> list[str]: def get_irq_action(irq: int) -> list[str]:
try: try:
-3
View File
@@ -145,9 +145,6 @@ class HardwareBase(ABC):
def get_modem_temperatures(self): def get_modem_temperatures(self):
return [] return []
def get_modem_state(self) -> dict:
return {}
def initialize_hardware(self): def initialize_hardware(self):
pass pass
+12 -12
View File
@@ -56,29 +56,29 @@
}, },
{ {
"name": "boot", "name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
"hash": "b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615", "hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"hash_raw": "b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615", "hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"size": 46962688, "size": 46897152,
"sparse": false, "sparse": false,
"full_check": true, "full_check": true,
"has_ab": true, "has_ab": true,
"ondevice_hash": "3ef4769bb1cc73bfc31f96f073b46af42dcc0bad4ab2dde32d11929745d0cd28" "ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
}, },
{ {
"name": "system", "name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
"hash": "9aebca09b9d142718e71efbfe44b723010eec6e70067c043c63186067291739d", "hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
"hash_raw": "80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7", "hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"size": 4718592000, "size": 4718592000,
"sparse": true, "sparse": true,
"full_check": false, "full_check": false,
"has_ab": true, "has_ab": true,
"ondevice_hash": "270b76c5c5568828e3883390b83ea445425a104dd42119d68f06093ce6b6fcef", "ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
"alt": { "alt": {
"hash": "80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7", "hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7.img", "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
"size": 4718592000 "size": 4718592000
} }
} }
] ]
@@ -8,7 +8,6 @@ from functools import cached_property, lru_cache
from pathlib import Path from pathlib import Path
from openpilot.cereal import log from openpilot.cereal import log
from openpilot.common.esim.base import WEBBING_ICCID_PREFIX
from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.utils import sudo_read, sudo_write
from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action
from openpilot.common.esim.base import LPABase from openpilot.common.esim.base import LPABase
@@ -208,8 +207,6 @@ class HardwareComma(HardwareBase):
def get_network_metered(self, network_type) -> bool: def get_network_metered(self, network_type) -> bool:
if network_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G): if network_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G):
if self.get_modem_state().get("iccid", "").startswith(WEBBING_ICCID_PREFIX):
return True
from openpilot.common.params import Params from openpilot.common.params import Params
return Params().get_bool("GsmMetered") return Params().get_bool("GsmMetered")
try: try:
+2 -4
View File
@@ -5,7 +5,6 @@ import logging
import os import os
import select import select
import signal import signal
import string
import struct import struct
import subprocess import subprocess
import tempfile import tempfile
@@ -17,8 +16,6 @@ from ipaddress import IPv4Address, AddressValueError
from enum import Enum from enum import Enum
from openpilot.common.esim.base import WEBBING_ICCID_PREFIX
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s", format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s",
@@ -46,6 +43,7 @@ NETWORK_TYPE = {0: "gsm", 1: "gsm", 3: "gsm", 8: "gsm",
11: "nr", 12: "nr", 13: "nr"} 11: "nr", 12: "nr", 13: "nr"}
DIAL_CID = 1 DIAL_CID = 1
WEBBING_ICCID_PREFIX = "8985235"
PPPD_CMD = [ PPPD_CMD = [
"sudo", "pppd", PPP_PORT, "460800", "noauth", "nodetach", "noipdefault", "usepeerdns", "sudo", "pppd", PPP_PORT, "460800", "noauth", "nodetach", "noipdefault", "usepeerdns",
@@ -356,7 +354,7 @@ class Modem:
imei = "" imei = ""
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
if not all(c in string.hexdigits for c in iccid): if not iccid.isdigit():
iccid = "" iccid = ""
imsi = first_line("AT+CIMI") imsi = first_line("AT+CIMI")
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f92c0a9b41613e063b6a1cd086729f5555d9df6482249e171ed1e276272cad7d oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
size 24709520 size 24709205
+2 -13
View File
@@ -1,20 +1,14 @@
import os import os
from pathlib import Path from pathlib import Path
CHESTNUT_FW_VERSION = "8516b768" CHESTNUT_FW_VERSION = "ed4e39b7"
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
USB_DEVICES_PATH = Path("/sys/bus/usb/devices") USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
PRIMARY_USB_CONTROLLER = "a600000.ssusb" PRIMARY_USB_CONTROLLER = "a600000.ssusb"
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
return (vendor_id, product_id) in ids
def get_usb_topology() -> set[str]: def get_usb_topology() -> set[str]:
try: try:
return set(os.listdir(USB_DEVICES_PATH)) return set(os.listdir(USB_DEVICES_PATH))
@@ -36,10 +30,6 @@ def read_int(path: Path, base: int = 10) -> int:
return 0 return 0
def cable_connected() -> bool:
return read_int(TYPEC_CC_ORIENTATION_PATH) != 0
def usb_devices() -> list[Path]: def usb_devices() -> list[Path]:
try: try:
devices = (d for d in USB_DEVICES_PATH.glob("*") if (d / "idVendor").exists()) devices = (d for d in USB_DEVICES_PATH.glob("*") if (d / "idVendor").exists())
@@ -91,8 +81,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
entry.linkErrorCount = device["linkErrorCount"] entry.linkErrorCount = device["linkErrorCount"]
entry.usb3Lane = device.get("usb3Lane", "unknown") entry.usb3Lane = device.get("usb3Lane", "unknown")
if is_chestnut_usb_id(entry.vendorId, entry.productId): if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
chestnut_present = True chestnut_present = True
device_state.chestnutPresent = chestnut_present device_state.chestnutPresent = chestnut_present
device_state.usbState.connected = cable_connected()
+9 -23
View File
@@ -28,7 +28,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"CurrentBootlog", {PERSISTENT, STRING}}, {"CurrentBootlog", {PERSISTENT, STRING}},
{"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"DisableDriverCameraIR", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}}, {"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}},
{"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}}, {"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -40,6 +39,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}}, {"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}},
{"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}}, {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}},
{"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}},
{"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}},
{"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"ForcePowerDown", {PERSISTENT, BOOL}}, {"ForcePowerDown", {PERSISTENT, BOOL}},
{"GitBranch", {PERSISTENT, STRING}}, {"GitBranch", {PERSISTENT, STRING}},
@@ -67,7 +68,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}}, {"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}},
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"LastGPSPosition", {PERSISTENT, BYTES}}, {"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
{"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}}, {"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
{"LastAgnosPowerMonitorShutdown", {CLEAR_ON_MANAGER_START, STRING}}, {"LastAgnosPowerMonitorShutdown", {CLEAR_ON_MANAGER_START, STRING}},
@@ -91,12 +92,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
@@ -135,17 +130,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UpdaterLastFetchTime", {PERSISTENT, TIME}},
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"UsbGpuActive", {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}}, {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutRetry", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"InitialCruiseSpeedKph", {PERSISTENT, INT, "40"}},
{"NavDestination", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
{"NavigationEnabled", {PERSISTENT, BOOL, "0"}},
{"Offroad_ChestnutModelError", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_Pairing", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UploadsIgnored", {CLEAR_ON_MANAGER_START, JSON}},
{"PairingEmail", {PERSISTENT, STRING}},
{"PairingProvider", {PERSISTENT, STRING}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- // // --- sunnypilot params --- //
@@ -209,16 +195,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Model Manager params // Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync? {"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}},
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}}, {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control // Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -247,6 +232,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
// sunnypilot model params // sunnypilot model params
+4 -4
View File
@@ -27,14 +27,14 @@ public:
auto param_path = Params().getParamPath(); auto param_path = Params().getParamPath();
if (util::file_exists(param_path)) { if (util::file_exists(param_path)) {
std::string real_path = util::readlink(param_path); std::string real_path = util::readlink(param_path);
util::check_system(util::string_format("rm -rf %s", real_path.c_str())); util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
unlink(param_path.c_str()); unlink(param_path.c_str());
} }
if (getenv("COMMA_CACHE") == nullptr) { if (getenv("COMMA_CACHE") == nullptr) {
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str())); util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
} }
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str())); util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str())); util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
unsetenv("OPENPILOT_PREFIX"); unsetenv("OPENPILOT_PREFIX");
} }
+35 -556
View File
@@ -1,47 +1,16 @@
"""QR code encoding, decoding, and UI textures.""" """Small QR encoder for the UI's byte-mode, error-correction-level-L codes."""
import functools
import itertools
import numpy as np import numpy as np
import pyray as rl import pyray as rl
# (ec codewords per block, block count) for levels L, M, Q, H, versions 1-40 # Indexes are QR versions. These are the only two Reed-Solomon parameters needed
_EC = [ # for error-correction level L.
((7, 1), (10, 1), (13, 1), (17, 1)), ((10, 1), (16, 1), (22, 1), (28, 1)), ((15, 1), (26, 1), (18, 2), (22, 2)), _ECC_LEN = (0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28)
((20, 1), (18, 2), (26, 2), (16, 4)), ((26, 1), (24, 2), (18, 4), (22, 4)), ((18, 2), (16, 4), (24, 4), (28, 4)), _NUM_BLOCKS = (0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8)
((20, 2), (18, 4), (18, 6), (26, 5)), ((24, 2), (22, 4), (22, 6), (26, 6)), ((30, 2), (22, 5), (20, 8), (24, 8)),
((18, 4), (26, 5), (24, 8), (28, 8)), ((20, 4), (30, 5), (28, 8), (24, 11)), ((24, 4), (22, 8), (26, 10), (28, 11)),
((26, 4), (22, 9), (24, 12), (22, 16)), ((30, 4), (24, 9), (20, 16), (24, 16)), ((22, 6), (24, 10), (30, 12), (24, 18)),
((24, 6), (28, 10), (24, 17), (30, 16)), ((28, 6), (28, 11), (28, 16), (28, 19)), ((30, 6), (26, 13), (28, 18), (28, 21)),
((28, 7), (26, 14), (26, 21), (26, 25)), ((28, 8), (26, 16), (30, 20), (28, 25)), ((28, 8), (26, 17), (28, 23), (30, 25)),
((28, 9), (28, 17), (30, 23), (24, 34)), ((30, 9), (28, 18), (30, 25), (30, 30)), ((30, 10), (28, 20), (30, 27), (30, 32)),
((26, 12), (28, 21), (30, 29), (30, 35)), ((28, 12), (28, 23), (28, 34), (30, 37)), ((30, 12), (28, 25), (30, 34), (30, 40)),
((30, 13), (28, 26), (30, 35), (30, 42)), ((30, 14), (28, 28), (30, 38), (30, 45)), ((30, 15), (28, 29), (30, 40), (30, 48)),
((30, 16), (28, 31), (30, 43), (30, 51)), ((30, 17), (28, 33), (30, 45), (30, 54)), ((30, 18), (28, 35), (30, 48), (30, 57)),
((30, 19), (28, 37), (30, 51), (30, 60)), ((30, 19), (28, 38), (30, 53), (30, 63)), ((30, 20), (28, 40), (30, 56), (30, 66)),
((30, 21), (28, 43), (30, 59), (30, 70)), ((30, 22), (28, 45), (30, 62), (30, 74)), ((30, 24), (28, 47), (30, 65), (30, 77)),
((30, 25), (28, 49), (30, 68), (30, 81)),
]
# GF(256) with the QR polynomial x^8 + x^4 + x^3 + x^2 + 1: powers of alpha and their logs # 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412
_EXP = [1] _FORMAT_BITS = 0b111011111000100
for _ in range(254):
_EXP.append(_EXP[-1] << 1 ^ (0x11D if _EXP[-1] & 0x80 else 0))
_LOG = {v: i for i, v in enumerate(_EXP)}
def _bch_format(data: int) -> int:
v = data << 10
for shift in range(14, 9, -1):
if v >> shift & 1:
v ^= 0x537 << (shift - 10)
return (data << 10 | v) ^ 0x5412
# 15-bit format info indexed by (level bits << 3 | mask). Level bits: L=01, M=00, Q=11, H=10.
_FORMATS = [_bch_format(d) for d in range(32)]
def _raw_modules(version: int) -> int: def _raw_modules(version: int) -> int:
@@ -52,24 +21,8 @@ def _raw_modules(version: int) -> int:
return result - (36 if version >= 7 else 0) return result - (36 if version >= 7 else 0)
def _block_lengths(version: int, level: int) -> list[int]:
"""Data codewords per Reed-Solomon block. The last blocks may be one longer."""
ec, nblocks = _EC[version - 1][level]
total = _raw_modules(version) // 8 - ec * nblocks
return [total // nblocks + (i >= nblocks - total % nblocks) for i in range(nblocks)]
def _interleaved(version: int, level: int) -> list[tuple[int, int]]:
"""(block, index within block) of each transmitted codeword: data column-major, then ECC column-major."""
ec, nblocks = _EC[version - 1][level]
lens = _block_lengths(version, level)
data = [(b, i) for i in range(max(lens)) for b in range(nblocks) if i < lens[b]]
ecc = [(b, lens[b] + i) for i in range(ec) for b in range(nblocks)]
return data + ecc
def _capacity(version: int) -> int: def _capacity(version: int) -> int:
return sum(_block_lengths(version, 0)) return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version]
def _append_bits(bits: list[int], value: int, length: int) -> None: def _append_bits(bits: list[int], value: int, length: int) -> None:
@@ -96,18 +49,37 @@ def _data_codewords(data: bytes, version: int) -> bytes:
def _codewords(data: bytes, version: int) -> bytes: def _codewords(data: bytes, version: int) -> bytes:
"""Split data codewords into Reed-Solomon blocks and interleave data + ECC.""" """Split data codewords into Reed-Solomon blocks and interleave data + ECC."""
data = _data_codewords(data, version) data = _data_codewords(data, version)
divisor = _divisor(_EC[version - 1][0][0]) num_blocks = _NUM_BLOCKS[version]
blocks = [] ecc_len = _ECC_LEN[version]
raw_codewords = _raw_modules(version) // 8
short_len = raw_codewords // num_blocks
num_short = num_blocks - raw_codewords % num_blocks
divisor = _divisor(ecc_len)
blocks: list[tuple[bytes, bytes]] = []
offset = 0 offset = 0
for length in _block_lengths(version, 0): for i in range(num_blocks):
length = short_len - ecc_len + (0 if i < num_short else 1)
block = data[offset:offset + length] block = data[offset:offset + length]
blocks.append(block + _remainder(block, divisor)) blocks.append((block, _remainder(block, divisor)))
offset += length offset += length
return bytes(blocks[b][i] for b, i in _interleaved(version, 0)) result = bytearray()
for i in range(short_len - ecc_len + 1):
for block, _ in blocks:
result.extend(block[i:i + 1])
for i in range(ecc_len):
for _, ecc in blocks:
result.append(ecc[i])
return bytes(result)
def _multiply(x: int, y: int) -> int: def _multiply(x: int, y: int) -> int:
return _EXP[(_LOG[x] + _LOG[y]) % 255] if x and y else 0 result = 0
for _ in range(8):
result = (result << 1) ^ (0x11D if result & 0x80 else 0)
if y & 0x80:
result ^= x
y <<= 1
return result
def _divisor(degree: int) -> bytes: def _divisor(degree: int) -> bytes:
@@ -136,7 +108,7 @@ def _alignment_positions(version: int) -> list[int]:
if version == 1: if version == 1:
return [] return []
count = version // 7 + 2 count = version // 7 + 2
step = (version * 8 + count * 3 + 5) // (count * 4 - 4) * 2 step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2
return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1] return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1]
@@ -199,7 +171,7 @@ class _Qr:
def _format(self) -> None: def _format(self) -> None:
for i in range(15): for i in range(15):
bit = ((_FORMATS[1 << 3 | 0] >> i) & 1) != 0 # level L, mask 0 bit = ((_FORMAT_BITS >> i) & 1) != 0
y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i
self._set_function(8, y_pos, bit) self._set_function(8, y_pos, bit)
x_pos = self.size - 1 - i if i < 8 else 15 - i if i < 9 else 14 - i x_pos = self.size - 1 - i if i < 8 else 15 - i if i < 9 else 14 - i
@@ -244,496 +216,3 @@ def make_texture(data: str, inverted: bool = False) -> rl.Texture:
rl_image.mipmaps = 1 rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
return rl.load_texture_from_image(rl_image) return rl.load_texture_from_image(rl_image)
# ---- Symbol structure for decoding ----
class QRError(Exception):
pass
_LEVELS = (1, 0, 3, 2) # format info level bits -> column in _EC
_MASKS = [
lambda i, j: (i + j) % 2 == 0,
lambda i, j: i % 2 == 0,
lambda i, j: j % 3 == 0,
lambda i, j: (i + j) % 3 == 0,
lambda i, j: (i // 2 + j // 3) % 2 == 0,
lambda i, j: (i * j) % 2 + (i * j) % 3 == 0,
lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0,
lambda i, j: ((i + j) % 2 + (i * j) % 3) % 2 == 0,
]
_ALIGNMENT = np.ones((5, 5), dtype=bool)
_ALIGNMENT[1:4, 1:4] = False
_ALIGNMENT[2, 2] = True
def _gf_inv(a: int) -> int:
return _EXP[-_LOG[a] % 255]
@functools.lru_cache
def _data_coords(version: int) -> tuple[np.ndarray, np.ndarray]:
"""(rows, cols) of the data and error correction modules in placement order: two-column zigzag from the right."""
dim = version * 4 + 17
func = np.zeros((dim, dim), dtype=bool) # finder, timing, alignment, format, and version modules
func[:9, :9] = func[:9, dim - 8:] = func[dim - 8:, :9] = True
func[6, :] = func[:, 6] = True
positions = _alignment_positions(version)
for r, c in itertools.product(positions, positions):
if (r, c) not in ((6, 6), (6, dim - 7), (dim - 7, 6)):
func[r - 2:r + 3, c - 2:c + 3] = True
if version >= 7:
func[:6, dim - 11:dim - 8] = func[dim - 11:dim - 8, :6] = True
ys = np.arange(dim)
rows, cols = [], []
# the vertical timing column is skipped, so the pairs left of it start at odd columns
for i, right in enumerate(col if col > 6 else col - 1 for col in range(dim - 1, 0, -2)):
r = np.repeat(ys[::-1] if i % 2 == 0 else ys, 2)
c = np.tile((right, right - 1), dim)
keep = ~func[r, c]
rows.append(r[keep])
cols.append(c[keep])
return np.concatenate(rows), np.concatenate(cols)
# ---- Matrix decoding ----
def _poly_eval(p: list[int], x: int) -> int:
# p is highest degree first
y = 0
for c in p:
y = _multiply(y, x) ^ c
return y
_EXP_TABLE = np.array(_EXP)
_LOG_TABLE = np.array([_LOG.get(v, 0) for v in range(256)])
def _syndromes(msg: list[int], nsym: int) -> list[int]:
"""syn[i] = msg(alpha^i), msg highest degree first."""
m = np.array(msg)
exponents = np.arange(nsym)[:, None] * (len(msg) - 1 - np.arange(len(msg)))
return np.bitwise_xor.reduce(_EXP_TABLE[(_LOG_TABLE[m] + exponents) % 255] * (m != 0), axis=1).tolist()
def _rs_correct(msg: list[int], nsym: int) -> list[int]:
"""Corrects up to nsym // 2 errors in a Reed-Solomon codeword, in place."""
n = len(msg)
syn = _syndromes(msg, nsym)
if not any(syn):
return msg
# Berlekamp-Massey, sigma is lowest degree first
sigma, prev, L, m, b = [1], [1], 0, 1, 1
for r in range(nsym):
d = syn[r]
for i in range(1, L + 1):
d ^= _multiply(sigma[i], syn[r - i])
if d == 0:
m += 1
continue
coef = _multiply(d, _gf_inv(b))
shifted = [0] * m + prev
saved = sigma[:]
sigma = sigma + [0] * max(0, len(shifted) - len(sigma))
for i, c in enumerate(shifted):
sigma[i] ^= _multiply(coef, c)
if 2 * L <= r:
L, prev, b, m = r + 1 - L, saved, d, 1
else:
m += 1
sigma = sigma[:L + 1]
if 2 * L > nsym:
raise QRError("too many errors")
# Chien search: codeword position p has locator alpha^(n-1-p)
positions = [p for p in range(n) if _poly_eval(sigma[::-1], _EXP[(p - n + 1) % 255]) == 0]
if len(positions) != L:
raise QRError("error locator mismatch")
# solve syn[i] = sum_k e_k * X_k^i for the magnitudes e_k
xlog = [(n - 1 - p) % 255 for p in positions]
A = [[_EXP[(xlog[k] * i) % 255] for k in range(L)] + [syn[i]] for i in range(L)]
for col in range(L):
piv = next((r for r in range(col, L) if A[r][col]), None)
if piv is None:
raise QRError("singular")
A[col], A[piv] = A[piv], A[col]
inv = _gf_inv(A[col][col])
A[col] = [_multiply(inv, v) for v in A[col]]
for r in range(L):
if r != col and A[r][col]:
f = A[r][col]
A[r] = [a ^ _multiply(f, c) for a, c in zip(A[r], A[col], strict=True)]
for k, p in enumerate(positions):
msg[p] ^= A[k][L]
if any(_syndromes(msg, nsym)):
raise QRError("uncorrectable")
return msg
def _read_format(m: np.ndarray) -> int:
"""Returns the closest format info (level bits << 3 | mask) from either copy."""
dim = m.shape[0]
copies = ([(8, i) for i in range(6)] + [(8, 7), (8, 8), (7, 8)] + [(5 - i, 8) for i in range(6)],
[(dim - 1 - i, 8) for i in range(7)] + [(8, dim - 8 + i) for i in range(8)]) # (row, col), msb first
candidates = []
for coords in copies:
bits = int("".join(str(int(m[r, c])) for r, c in coords), 2)
candidates += [((bits ^ f).bit_count(), i) for i, f in enumerate(_FORMATS)]
distance, fmt = min(candidates)
if distance > 3:
raise QRError("bad format info")
return fmt
_ALNUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
_ECI_ENCODINGS = {
0: "cp437", 2: "cp437", 1: "iso8859-1", 3: "iso8859-1",
**{i + 2: f"iso8859-{i}" for i in range(2, 17) if i != 12},
20: "shift_jis", 21: "cp1250", 22: "cp1251", 23: "cp1252", 24: "cp1256",
25: "utf-16-be", 26: "utf-8", 27: "ascii", 170: "ascii", 28: "big5", 29: "gb18030", 30: "euc_kr",
}
class _Bits:
def __init__(self, data: list[int]):
self._value = int.from_bytes(bytes(data), "big")
self.remaining = len(data) * 8
def read(self, n: int) -> int:
if n > self.remaining:
raise QRError("bitstream underflow")
self.remaining -= n
return self._value >> self.remaining & (1 << n) - 1
def read_below(self, n: int, limit: int) -> int:
v = self.read(n)
if v >= limit:
raise QRError("value out of range")
return v
def _parse_data(data: list[int], version: int) -> str:
bits = _Bits(data)
out: list[str] = []
encoding = None
band = 0 if version <= 9 else 1 if version <= 26 else 2
while bits.remaining >= 4:
mode = bits.read(4)
if mode == 0:
break
if mode == 7: # ECI character set assignment
first = bits.read(8)
extra = 0 if first < 0x80 else 8 if first < 0xC0 else 16 if first < 0xE0 else -1 # 1, 2, or 3 byte assignment
if extra < 0:
raise QRError("bad ECI assignment")
assignment = (first & 0x7F >> extra // 8) << extra | bits.read(extra)
encoding = _ECI_ENCODINGS.get(assignment)
if encoding is None:
raise QRError(f"unsupported ECI assignment {assignment}")
elif mode == 1:
n = bits.read((10, 12, 14)[band])
while n > 0:
k = min(n, 3) # 3 digits in 10 bits, the last 2 or 1 in 7 or 4
out.append(f"{bits.read_below((4, 7, 10)[k - 1], 10 ** k):0{k}d}")
n -= k
elif mode == 2:
n = bits.read((9, 11, 13)[band])
while n > 0:
k = min(n, 2) # 2 characters in 11 bits, a last one in 6
v = bits.read_below((6, 11)[k - 1], 45 ** k)
out.append(_ALNUM[v // 45] * (k - 1) + _ALNUM[v % 45])
n -= k
elif mode == 4:
n = bits.read((8, 16, 16)[band])
segment = bytes(bits.read(8) for _ in range(n))
try:
out.append(segment.decode(encoding or "utf-8"))
except UnicodeDecodeError as e:
if encoding is not None:
raise QRError("invalid ECI byte segment") from e
out.append(segment.decode("latin-1"))
elif mode == 8:
n = bits.read((8, 10, 12)[band])
for _ in range(n):
v = bits.read(13)
c = (v // 0xC0) << 8 | v % 0xC0
c += 0x8140 if c < 0x1F00 else 0xC140
try:
out.append(c.to_bytes(2, "big").decode("shift_jis"))
except UnicodeDecodeError as e:
raise QRError("invalid Kanji character") from e
else:
raise QRError(f"unsupported mode {mode}")
return "".join(out)
def decode_matrix(m: np.ndarray) -> str:
"""Decodes a square boolean module matrix (True = dark) without a quiet zone."""
dim = m.shape[0]
if m.shape != (dim, dim) or dim % 4 != 1 or not 21 <= dim <= 177:
raise QRError("bad matrix size")
version = (dim - 17) // 4
fmt = _read_format(m)
level = _LEVELS[fmt >> 3]
rows, cols = _data_coords(version)
bits = m[rows, cols] ^ _MASKS[fmt & 7](rows, cols)
codewords = np.packbits(bits[:len(bits) // 8 * 8]).tolist()
ec, _ = _EC[version - 1][level]
lens = _block_lengths(version, level)
blocks = [[0] * (n + ec) for n in lens]
for (b, i), codeword in zip(_interleaved(version, level), codewords, strict=True):
blocks[b][i] = codeword
data: list[int] = []
for block, n in zip(blocks, lens, strict=True):
data += _rs_correct(block, ec)[:n]
return _parse_data(data, version)
# ---- Image decoding ----
def _box_sums(a: np.ndarray, radii: tuple[int, ...]) -> list[np.ndarray]:
"""Sums over (2r + 1)^2 neighborhoods of the last two axes, edge padded, from one integral image."""
P = max(radii)
lead = [(0, 0)] * (a.ndim - 2)
cs = np.pad(np.cumsum(np.cumsum(np.pad(a, lead + [(P, P), (P, P)], mode="edge"), -2), -1), lead + [(1, 0), (1, 0)])
H, W = a.shape[-2:]
out = []
for r in radii:
lo, hi = P - r, P + r + 1
out.append(cs[..., hi:hi + H, hi:hi + W] - cs[..., lo:lo + H, hi:hi + W] - cs[..., hi:hi + H, lo:lo + W] + cs[..., lo:lo + H, lo:lo + W])
return out
def _binarize(gray: np.ndarray) -> np.ndarray:
"""Adaptive threshold: each pixel against the mean of the surrounding tiles that have contrast."""
h, w = gray.shape
if h < 21 or w < 21:
raise QRError("image too small")
B = max(8, min(h, w) // 128 * 2)
H, W = -(-h // B), -(-w // B)
padded = np.pad(gray, ((0, H * B - h), (0, W * B - w)), mode="edge")
# block statistics from a subsample are plenty
sub = np.ascontiguousarray(padded[::2, ::2].reshape(H, B // 2, W, B // 2).transpose(0, 2, 1, 3)).reshape(H, W, -1)
blocks = sub.sum(axis=2, dtype=np.uint32) / sub.shape[2]
known = sub.max(axis=2) - sub.min(axis=2) >= 32
# Flat tiles cannot estimate their own threshold: use the tiles with contrast nearby, then
# further out, then the global midrange. A flat tile is then all dark or all light.
est = np.full((H, W), (blocks.min() + blocks.max()) / 2)
filled = np.zeros((H, W), dtype=bool)
for total, count in _box_sums(np.stack((known * blocks, known.astype(float))), (2, 6)):
fill = ~filled & (count > 0)
est[fill] = total[fill] / count[fill]
filled |= fill
thr = np.where(known, np.minimum(est, 254) + 1, np.where(blocks <= est, 255, 0)).astype(np.uint8)
return (padded.reshape(H, B, W, B) < thr[:, None, :, None]).reshape(H * B, W * B)[:h, :w]
class _Runs:
"""Run-length table of a padded, flattened binary image with a per-pixel run index."""
def __init__(self, padded: np.ndarray):
self.flat = padded.ravel()
self.lines, self.stride = padded.shape
change = self.flat[1:] != self.flat[:-1]
self.starts = np.concatenate(([0], np.flatnonzero(change) + 1))
self.lengths = np.diff(np.append(self.starts, self.flat.size)).astype(np.int32)
def run_at(self, line: np.ndarray, pos: np.ndarray) -> np.ndarray:
"""Index of the run containing the pixel at `pos` along `line`."""
return np.searchsorted(self.starts, line * self.stride + pos + 1, side="right") - 1
@staticmethod
def _match(lengths: list[np.ndarray], ratios: tuple[int, ...]) -> tuple[np.ndarray, np.ndarray]:
"""Checks windows of runs against the ratios, given the length of each run. Returns (ok, module size)."""
S = sum(ratios)
total = sum(lengths[1:], start=lengths[0])
ok = total >= 2 * S # modules need to be at least 2 px
for L, r in zip(lengths, ratios, strict=True):
ok &= np.abs(2 * S * L - 2 * r * total) <= r * total # integer form of |L - r * total / S| <= r * total / (2 * S)
return ok, total / S
def scan(self, ratios: tuple[int, ...]) -> np.ndarray:
"""Returns the indices of all dark runs starting a window of runs matching the ratios."""
n = len(ratios)
N = len(self.lengths) - n + 1
if N <= 0:
return np.zeros(0, dtype=int)
ok, _ = self._match([self.lengths[k:N + k] for k in range(n)], ratios)
ok &= self.flat[self.starts[:N]]
first = np.flatnonzero(ok)
return first[self.starts[first] // self.stride == self.starts[first + n - 1] // self.stride]
def check(self, first: np.ndarray, ratios: tuple[int, ...]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Checks the run windows starting at run index `first`. Returns (ok, center position along the line, module size)."""
n, half = len(ratios), len(ratios) // 2
ok = (first >= 0) & (first + n <= len(self.starts))
idx = np.clip(first[:, None] + np.arange(n), 0, len(self.starts) - 1)
matched, module = self._match([self.lengths[idx[:, k]] for k in range(n)], ratios)
ok &= matched & self.flat[self.starts[idx[:, 0]]]
ok &= self.starts[idx[:, 0]] // self.stride == self.starts[idx[:, -1]] // self.stride
center = self.starts[idx[:, half]] % self.stride - 1 + self.lengths[idx[:, half]] / 2
return ok, center, module
def _find_patterns(binary: np.ndarray, ratios: tuple[int, ...]) -> list[tuple[float, float, float]]:
"""Finds dark/light run patterns with the given module ratios. Returns (x, y, module size)."""
half = len(ratios) // 2
step = 2 # the center rows of a 2 px finder pattern still get scanned twice
rows_t = _Runs(np.pad(binary[::step], ((0, 0), (1, 1))))
first = rows_t.scan(ratios)
if len(first) == 0:
return []
_, cx, hmod = rows_t.check(first, ratios)
row = rows_t.starts[first] // rows_t.stride * step
xi = cx.astype(int)
xs, col = np.unique(xi, return_inverse=True)
cols_t = _Runs(np.pad(binary[:, xs].T, ((0, 0), (1, 1))))
ok, cy, vmod = cols_t.check(cols_t.run_at(col, row) - half, ratios)
ok &= (0.5 <= vmod / hmod) & (vmod / hmod <= 2)
line = np.clip(np.rint(cy / step), 0, rows_t.lines - 1).astype(int)
ok2, cx2, hmod2 = rows_t.check(rows_t.run_at(line, xi) - half, ratios)
ok &= ok2 & (0.5 <= hmod2 / vmod) & (hmod2 / vmod <= 2)
found: list[list[float]] = [] # [x, y, module, count]
for x, y, module in zip(cx2[ok], cy[ok], (hmod2[ok] + vmod[ok]) / 2, strict=True):
for f in found:
if abs(f[0] - x) <= f[2] and abs(f[1] - y) <= f[2] and 0.5 <= f[2] / module <= 2:
c = f[3]
f[0], f[1], f[2], f[3] = (f[0] * c + x) / (c + 1), (f[1] * c + y) / (c + 1), (f[2] * c + module) / (c + 1), c + 1
break
else:
found.append([x, y, module, 1])
found.sort(key=lambda f: -f[3])
return [(f[0], f[1], f[2]) for f in found if f[3] >= 2]
def _pick_finders(patterns: list[tuple[float, float, float]]) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]:
"""Returns (top-left, top-right, bottom-left) centers and the module size of the most square-looking triple."""
best = None
for a, b, c in itertools.combinations(patterns[:10], 3):
mods = sorted((a[2], b[2], c[2]))
if mods[2] / mods[0] > 1.5:
continue
pts = [np.array(p[:2]) for p in (a, b, c)]
d = [np.linalg.norm(pts[(i + 1) % 3] - pts[(i + 2) % 3]) for i in range(3)]
tl = int(np.argmax(d)) # opposite the hypotenuse
p1, p2 = pts[(tl + 1) % 3], pts[(tl + 2) % 3]
v1, v2 = p1 - pts[tl], p2 - pts[tl]
n1, n2 = np.linalg.norm(v1), np.linalg.norm(v2)
if n1 == 0 or n2 == 0:
continue
cos = abs(np.dot(v1, v2)) / (n1 * n2)
if cos > 0.35 or not 0.6 <= n1 / n2 <= 1.6:
continue
score = cos + abs(np.log(n1 / n2)) + np.log(mods[2] / mods[0])
if best is not None and score >= best[0]:
continue
if v1[0] * v2[1] - v1[1] * v2[0] < 0:
p1, p2 = p2, p1
best = (score, pts[tl], p1, p2, float(sum(mods) / 3))
if best is None:
raise QRError("no finder patterns")
return best[1:]
def _perspective(src: np.ndarray, dst: np.ndarray) -> np.ndarray:
"""Homography mapping the four src points onto the four dst points."""
A = [row for (x, y), (u, v) in zip(src, dst, strict=True)
for row in ([x, y, 1, 0, 0, 0, -u * x, -u * y], [0, 0, 0, x, y, 1, -v * x, -v * y])]
try:
h = np.linalg.solve(np.array(A, dtype=float), np.asarray(dst, dtype=float).ravel())
except np.linalg.LinAlgError as e:
raise QRError("degenerate geometry") from e
return np.append(h, 1).reshape(3, 3)
def _transform(H: np.ndarray, pts: np.ndarray) -> np.ndarray:
p = np.column_stack((pts, np.ones(len(pts)))) @ H.T
return p[:, :2] / p[:, 2:3]
def _match_alignment(binary: np.ndarray, est: np.ndarray, offs: np.ndarray, r: int, module: float) -> np.ndarray | None:
h, w = binary.shape
dy = np.arange(max(0, int(est[1]) - r), min(h, int(est[1]) + r)) - est[1]
dx = np.arange(max(0, int(est[0]) - r), min(w, int(est[0]) + r)) - est[0]
if len(dy) == 0 or len(dx) == 0:
return None
y = np.rint(est[1] + dy[:, None, None] + offs[None, None, :, 1]).astype(int)
x = np.rint(est[0] + dx[None, :, None] + offs[None, None, :, 0]).astype(int)
valid = ((y >= 0) & (y < h) & (x >= 0) & (x < w)).all(axis=2)
samples = binary[np.clip(y, 0, h - 1), np.clip(x, 0, w - 1)]
score = np.where(valid, (samples == _ALIGNMENT.ravel()).sum(axis=2), 0)
if score.max() < 23:
return None
hits = np.argwhere(score == score.max())
centers = np.column_stack((est[0] + dx[hits[:, 1]], est[1] + dy[hits[:, 0]]))
closest = centers[np.argmin(np.linalg.norm(centers - est, axis=1))]
return centers[np.linalg.norm(centers - closest, axis=1) <= module / 2].mean(axis=0)
def _locate_alignment(binary: np.ndarray, H: np.ndarray, center: float, module: float) -> np.ndarray | None:
"""Template matches the 5x5 alignment pattern around its position estimated from H."""
grid = np.mgrid[-2:3, -2:3].reshape(2, -1).T[:, ::-1] + center # (25, 2) module coords (x, y)
pts = _transform(H, grid)
# the affine estimate can be off in both position and local scale under perspective
for radius in (2, 4, 8, 16):
for scale in (1.0, 0.8, 1.25, 0.65, 1.5):
found = _match_alignment(binary, pts[12], (pts - pts[12]) * scale, int(module * radius), module)
if found is not None:
return found
return None
def _sample(binary: np.ndarray, tl: np.ndarray, tr: np.ndarray, bl: np.ndarray, module: float, dim: int, use_alignment: bool) -> np.ndarray:
src = np.array([(3.5, 3.5), (dim - 3.5, 3.5), (3.5, dim - 3.5), (dim - 3.5, dim - 3.5)])
dst = np.array([tl, tr, bl, tr + bl - tl])
H = _perspective(src, dst)
if use_alignment and dim > 21:
align = _locate_alignment(binary, H, dim - 6.5, module)
if align is not None:
src[3], dst[3] = (dim - 6.5, dim - 6.5), align
H = _perspective(src, dst)
rows, cols = np.mgrid[0:dim, 0:dim]
pts = _transform(H, np.column_stack((cols.ravel() + 0.5, rows.ravel() + 0.5)))
xy = np.rint(pts).astype(int)
h, w = binary.shape
if (xy < 0).any() or (xy[:, 0] >= w).any() or (xy[:, 1] >= h).any():
raise QRError("code extends outside image")
return binary[xy[:, 1], xy[:, 0]].reshape(dim, dim)
def decode(gray: np.ndarray) -> str | None:
"""Decodes the QR code in a 2D uint8 grayscale image. Modules need to be at least 2 px.
Returns None if nothing could be decoded."""
try:
binary = _binarize(gray)
tl, tr, bl, module = _pick_finders(_find_patterns(binary, (1, 1, 3, 1, 1)))
except QRError:
return None
d = (np.linalg.norm(tr - tl) + np.linalg.norm(bl - tl)) / 2
dim = int(round((d / module + 7 - 17) / 4)) * 4 + 17
dims = [cand for cand in (dim, dim - 4, dim + 4) if 21 <= cand <= 177]
for cand, use_alignment, transpose in itertools.product(dims, (True, False), (False, True)):
try:
m = _sample(binary, tl, tr, bl, module, cand, use_alignment)
return decode_matrix(m.T if transpose else m)
except QRError:
pass
return None
+2 -7
View File
@@ -91,13 +91,8 @@ class Ratekeeper:
self.avg_dt.add_value(self._last_monitor_time - prev) self.avg_dt.add_value(self._last_monitor_time - prev)
lagged = False lagged = False
now = time.monotonic() remaining = self._next_frame_time - time.monotonic()
remaining = self._next_frame_time - now self._next_frame_time += self._interval
if remaining < -self._interval:
# more than a frame behind (e.g. a stall): resync instead of running catch-up frames back-to-back
self._next_frame_time = now + self._interval
else:
self._next_frame_time += self._interval
if self._print_delay_threshold is not None and remaining < -self._print_delay_threshold: if self._print_delay_threshold is not None and remaining < -self._print_delay_threshold:
print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms") print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms")
lagged = True lagged = True
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-178
View File
@@ -1,178 +0,0 @@
import hashlib
import math
from pathlib import Path
import numpy as np
from openpilot.common import qrcode as qr
from openpilot.common.test import OpenpilotTestCase
LPA = "LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5"
# Matrices generated with python-qrcode 8.2, covering all versions and EC levels.
# Packed fixtures keep the decoder tests independent of our encoder.
FIXTURES = {}
for path in Path(__file__).with_name("fixtures").glob("qrcode_*.npz"):
with np.load(path) as fixtures:
FIXTURES.update({key: fixtures[key] for key in fixtures.files})
def fixture(key: str) -> np.ndarray:
bits = np.unpackbits(FIXTURES[key])
size = math.isqrt(len(bits))
return bits[:size * size].reshape(size, size).astype(bool)
def render(matrix: np.ndarray, box: int = 6, border: int = 4) -> np.ndarray:
img = np.repeat(np.repeat(np.pad(matrix, border), box, axis=0), box, axis=1)
return np.where(img, 0, 255).astype(np.uint8)
def make(data: str, version: int | None = None, level: int = 0, box: int = 6, border: int = 4):
matrix = fixture(hashlib.sha256(f"{version}:{level}:{data}".encode()).hexdigest())
return matrix, render(matrix, box, border)
def warp(img: np.ndarray, H: np.ndarray) -> np.ndarray:
"""Bilinear resampling through the output -> input homography H, white outside the image."""
h, w = img.shape
rows, cols = np.mgrid[0:h, 0:w]
pts = qr._transform(H, np.column_stack((cols.ravel() + 0.5, rows.ravel() + 0.5))) - 0.5
x0, y0 = np.floor(pts[:, 0]).astype(int), np.floor(pts[:, 1]).astype(int)
fx, fy = pts[:, 0] - x0, pts[:, 1] - y0
padded = np.pad(img.astype(float), 1, constant_values=255)
def at(y, x):
return padded[np.clip(y + 1, 0, h + 1), np.clip(x + 1, 0, w + 1)]
out = at(y0, x0) * (1 - fx) * (1 - fy) + at(y0, x0 + 1) * fx * (1 - fy) + at(y0 + 1, x0) * (1 - fx) * fy + at(y0 + 1, x0 + 1) * fx * fy
return np.clip(out, 0, 255).astype(np.uint8).reshape(h, w)
def rotate(img: np.ndarray, angle: float) -> np.ndarray:
h, w = img.shape
t = np.radians(angle)
R = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]])
center = np.array([w / 2, h / 2])
corners = np.array([(0, 0), (w, 0), (w, h), (0, h)], dtype=float)
return warp(img, qr._perspective((corners - center) @ R.T + center, corners))
class TestQRCode(OpenpilotTestCase):
def test_alignment_positions(self):
assert qr._alignment_positions(7) == [6, 22, 38]
assert qr._alignment_positions(32) == [6, 34, 60, 86, 112, 138]
assert qr._alignment_positions(40) == [6, 30, 58, 86, 114, 142, 170]
def test_all_versions(self):
for version in range(1, 41):
for level in range(4):
with self.subTest(version=version, level=level):
data = "".join(chr(ord("a") + i % 26) for i in range(version))
matrix, img = make(data, version, level, box=3)
assert qr.decode_matrix(matrix) == data
assert qr.decode(img) == data
def test_modes(self):
for data in ["0123456789012345", "HELLO WORLD $1.50", LPA, "こんにちは", "ünïcødé", "mixed 123 ABC xyz"]:
with self.subTest(data=data):
matrix, img = make(data)
assert qr.decode_matrix(matrix) == data
assert qr.decode(img) == data
def test_error_correction(self):
matrix, _ = make(LPA, level=2)
rng = np.random.default_rng(0)
flipped = matrix.copy()
for r, c in rng.integers(9, matrix.shape[0] - 9, size=(40, 2)):
flipped[r, c] ^= True
assert qr.decode_matrix(flipped) == LPA
def test_large_modules(self):
for data in ["0123456789012345", "HELLO WORLD $1.50", LPA, "mixed 123 ABC xyz"]:
for box in [16, 20, 24, 32]:
for dark, light in [(0, 255), (60, 200), (140, 250)]:
with self.subTest(data=data, box=box, dark=dark):
_, img = make(data, box=box)
img = np.where(img == 0, dark, light).astype(np.uint8)
assert qr.decode(img) == data
def test_image_edges(self):
# a code touching the image edge must not lose the rows and columns left over from tiling
matrix, _ = make(LPA)
for size in (200, 203):
with self.subTest(size=size):
img = np.full((size, size), 255, dtype=np.uint8)
code = render(matrix, box=5, border=0)
img[size - code.shape[0]:, size - code.shape[1]:] = code
assert qr.decode(img) == LPA
def test_eci(self):
# qrcode_eci.npz: packed Segno 1.6.6 matrices, generated with mode='byte',
# eci=True, micro=False and the named encoding. Mixed also includes numeric,
# alphanumeric, and Kanji segments after changing the byte encoding twice.
cases = {
"iso8859-5": "Привет", "utf-16-be": "héllo", "utf-8": "こんにちは",
"shift_jis": "日本語", "cp1251": "Привет", "iso8859-1": "héllo",
"mixed": "hélloПривет日本語123ABC漢字",
}
for encoding, expected in cases.items():
with self.subTest(encoding=encoding):
matrix = fixture(encoding)
assert qr.decode_matrix(matrix) == expected
assert qr.decode(render(matrix)) == expected
def test_parse_data(self):
def parse(stream: str) -> str:
stream += '0' * (-len(stream) % 8)
return qr._parse_data([int(stream[i:i + 8], 2) for i in range(0, len(stream), 8)], 1)
def eci(assignment: str, payload: bytes = b'A') -> str:
return parse('0111' + assignment + '0100' + f'{len(payload):08b}' + ''.join(f'{b:08b}' for b in payload) + '0000')
# ASCII assignment 170 uses the two-byte ECI representation.
assert eci('1000000010101010') == 'A'
for assignment in ['00001110', '1000001111100111', '110000010000000000000000', '11100000']:
with self.subTest(assignment=assignment), self.assertRaises(qr.QRError):
eci(assignment)
with self.assertRaises(qr.QRError):
eci('00011010', b'\xff') # Invalid UTF-8 must not fall back to Latin-1.
# out-of-range numeric, alphanumeric, and Kanji values are format errors, not crashes
for stream in ['0001' + '0000000011' + '1111111111', '0001' + '0000000010' + '1111111',
'0010' + '000000010' + '11111111111', '0010' + '000000001' + '111111',
'1000' + '00000001' + '0000000111111']:
with self.subTest(stream=stream), self.assertRaises(qr.QRError):
parse(stream)
def test_rotation(self):
for angle in [0, 90, 180, 270, 25, 110]:
with self.subTest(angle=angle):
_, img = make(LPA, box=8, border=12)
assert qr.decode(rotate(img, angle)) == LPA
def test_mirrored(self):
_, img = make(LPA)
assert qr.decode(img[:, ::-1]) == LPA
def test_perspective_and_noise(self):
_, img = make(LPA, box=10, border=8)
h, w = img.shape
corners = np.array([(40, 60), (w - 20, 30), (w - 60, h - 40), (30, h - 90)])
arr = warp(img, qr._perspective(corners, np.array([(0, 0), (w, 0), (w, h), (0, h)]))).astype(float)
rng = np.random.default_rng(1)
arr = arr * 0.6 + 60 + rng.normal(0, 12, arr.shape) # low contrast + noise
# uneven lighting
arr += np.linspace(-40, 40, w)[None, :]
assert qr.decode(np.clip(arr, 0, 255).astype(np.uint8)) == LPA
def test_no_code(self):
rng = np.random.default_rng(2)
assert qr.decode(rng.integers(0, 256, size=(240, 320), dtype=np.uint8)) is None
assert qr.decode(np.full((240, 320), 200, dtype=np.uint8)) is None
def test_encoder_roundtrip(self):
for version in range(1, 21):
with self.subTest(version=version):
assert qr.decode_matrix(np.array(qr._Qr(version, b"hello").modules)) == "hello"
+15
View File
@@ -5,6 +5,7 @@ import contextlib
import subprocess import subprocess
import time import time
import functools import functools
from subprocess import Popen, PIPE, TimeoutExpired
import zstandard as zstd import zstandard as zstd
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
@@ -156,6 +157,20 @@ def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> st
return default return default
@contextlib.contextmanager
def managed_proc(cmd: list[str], env: dict[str, str]):
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
try:
yield proc
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except TimeoutExpired:
proc.kill()
def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None): def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None):
rows = [list(row) for row in tabular_data] rows = [list(row) for row in tabular_data]
+1 -1
View File
@@ -1 +1 @@
#define COMMA_VERSION "0.11.3" #define COMMA_VERSION "0.11.2"
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:911f59f248015600da7ecc689398103d47dfc57f6be17ac8c8e543a726a6c64b
size 3311
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd5d8d3fce5e30662797f7eeed224f5775b2cabcc055f01f5ebf6e7b2657b616
size 1801
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e58deb1778cf2826339f27e9f09eecc79ea137c1436210c14b71c352a649c77
size 34953
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7ab989c9fe22e7d2119bac5284cdccfba2889477d69f5f6c6b4470c7ee0aab3
size 1801
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8f37a02dd914405c6f86f415700dd5985eb976b923e7abd6580d2da76533594e
size 9466
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90d441310f8e1833c661d8a4f0546aab2eb50eb21cc21beb0aff0d27b5ee6066
size 1571
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b2dad33fead9f064c3a548651d6ef37daf82b6127c329683f538ec6e986ecbc
size 11204
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82648254da89edb0cf65ef63fc5e6e0741414051bfea8a40449132fd61b1ed0f
size 1533
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf97a6738b294ac0aed9b2d075916cee0b7d3215bcd23381760900ede6a92748
size 13256
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
size 2034
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472
size 7279
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
size 10876
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01841b602632c66ab14a8e52b874a1623f09641dc2ef0620f4e2d00bb4a913f3
size 16243
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e0936a3d890a3ad68ffe021e4905b9b9e59ffc2ae19ee9c703daa3e7c92e7c78
size 1793
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57c7ba72642b0350ee764c924586fa66ff94915be4aba1813797e67eb4b74376
size 33215
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71dc069bd3dc5b11ba8e1b52eb3a1025ee0ad8c82834f94b4b2269ec0d401920
size 237
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:73a6fe02cf19105c0051a6664a424c1bf2ede364c6cf3791f235c86ffa203f39
size 10348
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f500f118dca3eedbd86fc6cc943ad8be67fe5d4f7b474d5b452cd757ad805c3
size 282
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a49996f0ce069a048abe94ba9424de655dcff53745676ab35e045204af88a3c
size 12103
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8542374779f1908dc52c8a126bc041efea4c4d605afdd34e3bf8e520a7e78bf2
size 320
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21e656b387c1a7e8de54d3e1b1c246f442cffe3c5e0924273d6d6d897c00b568
size 13868
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:92431d37a844206570d73d48f39dd8d934539a89668fe8d5f4cccae38f7b301b oid sha256:fd91685bf656e828648acf035a4737acb2c4709e8514cf0aa0a10fa470a9bb60
size 11530 size 11580
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3efe7d12ae82e0f5a1bd773129cf1eafe3c8c36ff5cba43a6c95f295706062b5
size 19724
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfb645e59636687d61d20283bae65b68ddda6f7b2d4fda01a6e613a842e3b41f
size 823
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df07383a6ddfe8cd7eeb2f22271cbd13068327ca0978873f8d10368860562e40
size 111083
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7518f3409e2ff8f7e99fb7f0c06afa8bdf0b0a506147b5cf9d4931b820a1a75
size 1107
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a18e3de5f455b4089a824da8d5d78d5d532ef71facb05cc75c178b24d5beeb7
size 920
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:710e74b526adb5bbac15cd8915a828dd673dd8ce15b2fd694c64c182eef6f56f
size 13744
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25db08a9038b5faa3b200ef5625cb216d7f4b31ea3d49fb0395f9b726bfaf8e6
size 13135
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:547f466cc59d29647f3bf131d8444d7f22b63b186bcea55ceb8109c68ca74fe8
size 2345
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55050096cd98e7f4ea3e843d927c18815e458dfbd5e7d100b503db92075d061b
size 2373
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9c439ed89bc0357e86c005d5dbb6f1ad2b8d72cdc0890e3201d842d8c3928f1
size 2443
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7aeccd3e0d8517f74eb34f7d73632c0b29b0ad5df5b9686c0f509bb6b136561e
size 1728
Binary file not shown.
+2 -2
View File
@@ -23,8 +23,8 @@ done
# sudo apt install inkscape # sudo apt install inkscape
for svg in $(find "$DIR" -type f | grep svg$); do for svg in $(find $DIR -type f | grep svg$); do
bunx svgo "$svg" --multipass --pretty --indent 2 bunx svgo $svg --multipass --pretty --indent 2
# convert to PNG # convert to PNG
png="${svg%.svg}.png" png="${svg%.svg}.png"
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d02369e21d3c78a3a5ece7e78deedb82343bc3228a28db73da48b5c27860004
size 52374
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3bef898e4e784baf98f3ceca53cbe95b1020da5e01162cdaa14b79b1ce0b1340
size 52374
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:884e828e5c51d980d41b6e2400fcb4f4a926275018a00674d8239184e1eb96db oid sha256:1bb440f424c989c06e203bfba0b32d39aede8f6c78a9b335e5e778460202b601
size 73026 size 73026
+10 -3
View File
@@ -127,6 +127,9 @@ class Car:
set_alternative_experience(self.CP, self.CP_SP, self.params) set_alternative_experience(self.CP, self.CP_SP, self.params)
set_car_specific_params(self.CP, self.CP_SP, self.params) set_car_specific_params(self.CP, self.CP_SP, self.params)
# Dynamic Experimental Control
self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl")
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly
self.CP.passive = not controller_available or self.CP.dashcamOnly self.CP.passive = not controller_available or self.CP.dashcamOnly
@@ -178,11 +181,14 @@ class Car:
self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP) self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP)
self.is_metric = self.params.get_bool("IsMetric") self.is_metric = self.params.get_bool("IsMetric")
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True) self.experimental_mode = self.params.get_bool("ExperimentalMode")
# card is driven by can recv, expected at 100Hz # card is driven by can recv, expected at 100Hz
self.rk = Ratekeeper(100, print_delay_threshold=None) self.rk = Ratekeeper(100, print_delay_threshold=None)
# log fingerprint in sentry
sunnypilot_interfaces.log_fingerprint(self.CP)
def state_update(self) -> tuple[car.CarState, custom.CarStateSP, structs.RadarDataT | None]: def state_update(self) -> tuple[car.CarState, custom.CarStateSP, structs.RadarDataT | None]:
"""carState update loop, driven by can""" """carState update loop, driven by can"""
@@ -211,7 +217,7 @@ class Car:
self.v_cruise_helper.update_v_cruise(CS, self.sm['carControl'].enabled, self.is_metric) self.v_cruise_helper.update_v_cruise(CS, self.sm['carControl'].enabled, self.is_metric)
if self.sm['carControl'].enabled and not self.CC_prev.enabled: if self.sm['carControl'].enabled and not self.CC_prev.enabled:
# Use CarState w/ buttons from the step selfdrived enables on # Use CarState w/ buttons from the step selfdrived enables on
self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.initial_cruise_speed) self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.experimental_mode, self.dynamic_experimental_control)
# TODO: mirror the carState.cruiseState struct? # TODO: mirror the carState.cruiseState struct?
CS.vCruise = float(self.v_cruise_helper.v_cruise_kph) CS.vCruise = float(self.v_cruise_helper.v_cruise_kph)
@@ -296,9 +302,10 @@ class Car:
def params_thread(self, evt): def params_thread(self, evt):
while not evt.is_set(): while not evt.is_set():
self.is_metric = self.params.get_bool("IsMetric") self.is_metric = self.params.get_bool("IsMetric")
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True) self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
# sunnypilot # sunnypilot
self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl")
self.v_cruise_helper.read_custom_set_speed_params() self.v_cruise_helper.read_custom_set_speed_params()
time.sleep(0.1) time.sleep(0.1)
+6 -2
View File
@@ -13,6 +13,7 @@ V_CRUISE_MIN = 8
V_CRUISE_MAX = 145 V_CRUISE_MAX = 145
V_CRUISE_UNSET = 255 V_CRUISE_UNSET = 255
V_CRUISE_INITIAL = 40 V_CRUISE_INITIAL = 40
V_CRUISE_INITIAL_EXPERIMENTAL_MODE = 105
IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding errors incrementing set speed IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding errors incrementing set speed
ButtonEvent = car.CarState.ButtonEvent ButtonEvent = car.CarState.ButtonEvent
@@ -137,14 +138,17 @@ class VCruiseHelper(VCruiseHelperSP):
self.button_timers[b.type.raw] = 1 if b.pressed else 0 self.button_timers[b.type.raw] = 1 if b.pressed else 0
self.button_change_states[b.type.raw] = {"standstill": CS.cruiseState.standstill, "enabled": enabled} self.button_change_states[b.type.raw] = {"standstill": CS.cruiseState.standstill, "enabled": enabled}
def initialize_v_cruise(self, CS, initial_speed: int) -> None: def initialize_v_cruise(self, CS, experimental_mode: bool, dynamic_experimental_control: bool) -> None:
# initializing is handled by the PCM # initializing is handled by the PCM
if self.CP.pcmCruise: if self.CP.pcmCruise:
return return
initial_experimental_mode = experimental_mode and not dynamic_experimental_control
initial = V_CRUISE_INITIAL_EXPERIMENTAL_MODE if initial_experimental_mode else V_CRUISE_INITIAL
if any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) and self.v_cruise_initialized: if any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) and self.v_cruise_initialized:
self.v_cruise_kph = self.v_cruise_kph_last self.v_cruise_kph = self.v_cruise_kph_last
else: else:
self.v_cruise_kph = int(round(np.clip(max(CS.vEgo * CV.MS_TO_KPH, initial_speed), V_CRUISE_INITIAL, V_CRUISE_MAX))) self.v_cruise_kph = int(round(np.clip(CS.vEgo * CV.MS_TO_KPH, initial, V_CRUISE_MAX)))
self.v_cruise_cluster_kph = self.v_cruise_kph self.v_cruise_cluster_kph = self.v_cruise_kph
@@ -14,7 +14,7 @@ ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type ButtonType = car.CarState.ButtonEvent.Type
def run_cruise_simulation(cruise, personality, t_end=20.): def run_cruise_simulation(cruise, e2e, personality, t_end=20.):
man = Maneuver( man = Maneuver(
'', '',
duration=t_end, duration=t_end,
@@ -24,6 +24,7 @@ def run_cruise_simulation(cruise, personality, t_end=20.):
cruise_values=[cruise], cruise_values=[cruise],
prob_lead_values=[0.0], prob_lead_values=[0.0],
breakpoints=[0.], breakpoints=[0.],
e2e=e2e,
personality=personality, personality=personality,
) )
valid, output = man.evaluate() valid, output = man.evaluate()
@@ -31,7 +32,8 @@ def run_cruise_simulation(cruise, personality, t_end=20.):
return output[-1, 3] return output[-1, 3]
@parameterized_class(("personality", "speed"), itertools.product( @parameterized_class(("e2e", "personality", "speed"), itertools.product(
[True, False], # e2e
log.LongitudinalPersonality.schema.enumerants, # personality log.LongitudinalPersonality.schema.enumerants, # personality
[5,35])) # speed [5,35])) # speed
class TestCruiseSpeed(OpenpilotTestCase): class TestCruiseSpeed(OpenpilotTestCase):
@@ -39,7 +41,7 @@ class TestCruiseSpeed(OpenpilotTestCase):
print(f'Testing {self.speed} m/s') print(f'Testing {self.speed} m/s')
cruise_speed = float(self.speed) cruise_speed = float(self.speed)
simulation_steady_state = run_cruise_simulation(cruise_speed, self.personality) simulation_steady_state = run_cruise_simulation(cruise_speed, self.e2e, self.personality)
self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {self.speed} m/s') self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {self.speed} m/s')
@@ -57,16 +59,16 @@ class TestVCruiseHelper(OpenpilotTestCase):
for _ in range(2): for _ in range(2):
self.v_cruise_helper.update_v_cruise(car.CarState(cruiseState={"available": False}), enabled=False, is_metric=False) self.v_cruise_helper.update_v_cruise(car.CarState(cruiseState={"available": False}), enabled=False, is_metric=False)
def enable(self, v_ego): def enable(self, v_ego, experimental_mode, dynamic_experimental_control):
# Simulates user pressing set with a current speed # Simulates user pressing set with a current speed
self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego), V_CRUISE_INITIAL) self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego), experimental_mode, dynamic_experimental_control)
def test_adjust_speed(self): def test_adjust_speed(self):
""" """
Asserts speed changes on falling edges of buttons. Asserts speed changes on falling edges of buttons.
""" """
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS) self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
for btn in (ButtonType.accelCruise, ButtonType.decelCruise): for btn in (ButtonType.accelCruise, ButtonType.decelCruise):
for pressed in (True, False): for pressed in (True, False):
@@ -90,7 +92,7 @@ class TestVCruiseHelper(OpenpilotTestCase):
CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=pressed)] CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=pressed)]
self.v_cruise_helper.update_v_cruise(CS, enabled=enabled, is_metric=False) self.v_cruise_helper.update_v_cruise(CS, enabled=enabled, is_metric=False)
if pressed: if pressed:
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS) self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
# Expected diff on enabling. Speed should not change on falling edge of pressed # Expected diff on enabling. Speed should not change on falling edge of pressed
assert (not pressed) == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last) assert (not pressed) == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
@@ -100,7 +102,7 @@ class TestVCruiseHelper(OpenpilotTestCase):
Asserts we don't increment set speed if user presses resume/accel to exit cruise standstill. Asserts we don't increment set speed if user presses resume/accel to exit cruise standstill.
""" """
self.enable(0) self.enable(0, False, False)
for standstill in (True, False): for standstill in (True, False):
for pressed in (True, False): for pressed in (True, False):
@@ -120,7 +122,7 @@ class TestVCruiseHelper(OpenpilotTestCase):
for v_ego in np.linspace(0, 100, 101): for v_ego in np.linspace(0, 100, 101):
self.reset_cruise_speed_state() self.reset_cruise_speed_state()
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS) self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
# first decrement speed, then perform gas pressed logic # first decrement speed, then perform gas pressed logic
expected_v_cruise_kph = self.v_cruise_helper.v_cruise_kph - IMPERIAL_INCREMENT expected_v_cruise_kph = self.v_cruise_helper.v_cruise_kph - IMPERIAL_INCREMENT
@@ -141,10 +143,12 @@ class TestVCruiseHelper(OpenpilotTestCase):
Asserts allowed cruise speeds on enabling with SET. Asserts allowed cruise speeds on enabling with SET.
""" """
for v_ego in np.linspace(0, 100, 101): for experimental_mode in (True, False):
self.reset_cruise_speed_state() for dynamic_experimental_control in (True, False):
assert not self.v_cruise_helper.v_cruise_initialized for v_ego in np.linspace(0, 100, 101):
self.reset_cruise_speed_state()
assert not self.v_cruise_helper.v_cruise_initialized
self.enable(float(v_ego)) self.enable(float(v_ego), experimental_mode, dynamic_experimental_control)
assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX
assert self.v_cruise_helper.v_cruise_initialized assert self.v_cruise_helper.v_cruise_initialized

Some files were not shown because too many files have changed in this diff Show More