mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-18 03:23:45 +08:00
Compare commits
261 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a3550358e | |||
| 7df636ac01 | |||
| ca0202c972 | |||
| 35e78a9d95 | |||
| 27c132984d | |||
| 0d57eadd74 | |||
| f20d9ea5e6 | |||
| 5ec0c02195 | |||
| 90932d9b5c | |||
| fcf4b0d395 | |||
| f5bcd10a2a | |||
| cd3d5b0057 | |||
| 4429ec0c8c | |||
| 41acce17fa | |||
| f7bc31f208 | |||
| 75b2ccca60 | |||
| f50108b4af | |||
| fe7ccf9587 | |||
| 24c26487f4 | |||
| 6eb63e0454 | |||
| 690fa743b9 | |||
| 4f3626dc9b | |||
| 1e219d7785 | |||
| 6939d172e8 | |||
| 66752e769f | |||
| 21edbbcfb9 | |||
| 2b09bc94a9 | |||
| cb1d6f11a7 | |||
| d3f0aab96f | |||
| 50319fa702 | |||
| 9bebe42f73 | |||
| 1623441a9f | |||
| 675d38213e | |||
| a47d1c56f6 | |||
| 4c53557764 | |||
| afcbb171f6 | |||
| 42ccc5cebd | |||
| 2713630e79 | |||
| 4c362ca327 | |||
| 6f4c0faf1f | |||
| 64be6c80d3 | |||
| 0679ff36c6 | |||
| b9c815d56a | |||
| 263e8d1966 | |||
| 655bfdebc7 | |||
| 0e5718116a | |||
| 043759da36 | |||
| d05c2d971b | |||
| 9b585d58f5 | |||
| 948ad05c6c | |||
| f804d3468e | |||
| 6846d733d4 | |||
| 28917bb3ab | |||
| 8d1865bdc6 | |||
| a742df6182 | |||
| f89ce23f9d | |||
| 3f06ed3bc7 | |||
| f21bfc36be | |||
| 657d3a4f7c | |||
| f59056e5b6 | |||
| 0e0c7c711d | |||
| a0d47bcd08 | |||
| 035a447d1f | |||
| 5b2a59d288 | |||
| 3323aafb54 | |||
| ab8c84c398 | |||
| c933446486 | |||
| 2b7c34a5c3 | |||
| fc61640782 | |||
| 84f9bdbc10 | |||
| cce54f20d5 | |||
| 4bcf73282e | |||
| 910c08e082 | |||
| 01a670d6b2 | |||
| ec95db3f1f | |||
| 78dccf0b48 | |||
| 7ee974ab5b | |||
| 16322aef16 | |||
| d2c0ce91ad | |||
| 5b75e1709c | |||
| c8fb906815 | |||
| 0c464e5c66 | |||
| 134517ceb3 | |||
| 49bbba371c | |||
| f474f05928 | |||
| b78331e042 | |||
| 46b97e6195 | |||
| 027770d49c | |||
| bbabd94c11 | |||
| f2c772ff11 | |||
| afa47035ba | |||
| c5543b1a05 | |||
| 4dbdd66e0c | |||
| f00d226d39 | |||
| a86351c3ef | |||
| 8377c40da1 | |||
| 841076f6d1 | |||
| 27fa4fca8e | |||
| 93eec90077 | |||
| e1862e2e17 | |||
| ec9241f296 | |||
| 66fd3d1bd4 | |||
| 082c43da39 | |||
| 521db4c825 | |||
| 3b2a75a4e0 | |||
| 1b1b60ba93 | |||
| 95ed0213ec | |||
| ea0afb67a8 | |||
| 680aa6f868 | |||
| d375e7ff5d | |||
| 2eee697f7d | |||
| 5ae0da0e46 | |||
| a39561058f | |||
| 8abd133637 | |||
| e691acbf34 | |||
| 7f6f13997c | |||
| d06711be19 | |||
| 9a95fdc1a2 | |||
| c6d13eb723 | |||
| c8d56a1990 | |||
| df7e0e5e77 | |||
| 6c69ebed7f | |||
| c504b92aba | |||
| 1090e2824b | |||
| b55e37b9c9 | |||
| e77f6e56f9 | |||
| 60b3ccf39e | |||
| bca3b04720 | |||
| 7635dec618 | |||
| 3cd1b6e483 | |||
| 7dcb524058 | |||
| d643860afa | |||
| f992a60bbd | |||
| 1328ace068 | |||
| 7db7735598 | |||
| cd1490af4b | |||
| bd176cb6a7 | |||
| 4d9d1bc341 | |||
| cab53438ae | |||
| 6080cc6168 | |||
| 81ae1a2e2d | |||
| f15544b482 | |||
| b751b04cda | |||
| fa9f56ed76 | |||
| 64f9b47b6e | |||
| 7b469af438 | |||
| 2c88d1ed64 | |||
| ffef0e6d3b | |||
| e6aa405e08 | |||
| 2689917757 | |||
| 5f50bda7ba | |||
| 5b34f151de | |||
| 64e5d1e5e3 | |||
| d3106c2b4b | |||
| 885792de3f | |||
| 5319474ee5 | |||
| 164a3a1e30 | |||
| c715ef4762 | |||
| 2ae3598b51 | |||
| 4e60afe70d | |||
| 5c365d3398 | |||
| e6f9b3c6c4 | |||
| 94f71fce00 | |||
| b6918cb31b | |||
| 11093e743e | |||
| ebb202f29d | |||
| 9c054e285d | |||
| c5cf29cf5e | |||
| 45a7747ca3 | |||
| 064a51fe59 | |||
| e5a6a8b73e | |||
| d38264c42c | |||
| 0d4a4ab910 | |||
| 44914a7cf4 | |||
| 4c0799eb8e | |||
| 1aa97338c4 | |||
| 20064d9681 | |||
| dc338c2831 | |||
| 298e51010c | |||
| 7e84a61668 | |||
| f147094cb7 | |||
| f235784042 | |||
| 39146cb240 | |||
| 19f0f69d85 | |||
| 64b4dbf115 | |||
| ad5c0b946d | |||
| 5cad99bf7a | |||
| f1ba169fb5 | |||
| dd226ac791 | |||
| 0cf294d85f | |||
| 6965492e66 | |||
| 73720bfd08 | |||
| 6107da1cd6 | |||
| 6c410eee37 | |||
| fc4af50824 | |||
| f87e170bee | |||
| 2a70b40cdc | |||
| e6f401b5cf | |||
| e9711bc51d | |||
| 9b899fdaa2 | |||
| 511c1f1e4e | |||
| 443967ddad | |||
| 633117b2a9 | |||
| f174e39d7a | |||
| 68d829c7c9 | |||
| 0cd3f0f20b | |||
| b97a2d8201 | |||
| d9ecb0b678 | |||
| ff2f78c3e8 | |||
| 1e0914051f | |||
| 20bddff857 | |||
| 14617070ab | |||
| 802a231bd2 | |||
| 179718dcb0 | |||
| d70df67366 | |||
| 66b4dbe2a1 | |||
| ef769c173d | |||
| a4a24d5d87 | |||
| 1fb3edb7dd | |||
| dd4615850c | |||
| f23e65768c | |||
| 96be8c7eb5 | |||
| 23d0474b95 | |||
| 60d8ae2d4f | |||
| 7bfe6ba4d0 | |||
| 9946c521f7 | |||
| 25d9d41c90 | |||
| 8f2d66d0e5 | |||
| 3eafb658bf | |||
| 10b9e73859 | |||
| bb86bee688 | |||
| 3e54af9db8 | |||
| 20ae4ac44d | |||
| 7c1f224443 | |||
| 2b4d050e9c | |||
| 1bb019521e | |||
| 624d5b9995 | |||
| 0f9ec7158b | |||
| 9c02eebb72 | |||
| cb70ba89aa | |||
| 65c411433b | |||
| 6bbdf8ad18 | |||
| 9d1f0d4171 | |||
| c51e3e5a71 | |||
| 3bc6701f34 | |||
| cb0da74b41 | |||
| a4f7c50d2a | |||
| 444be0a649 | |||
| a989bc0b50 | |||
| f9dacd0d6b | |||
| 0ec3a082c7 | |||
| c8603fb3ce | |||
| 69ad376fef | |||
| 33790c855c | |||
| 675ff56981 | |||
| e3def37695 | |||
| 3a13b67b6f | |||
| 7bade9a67a | |||
| c163743e32 | |||
| 06d76bdb24 | |||
| 50fb67f718 |
+3
-1
@@ -1,8 +1,10 @@
|
||||
* text=auto
|
||||
* text=auto eol=lf
|
||||
|
||||
# to move existing files into LFS:
|
||||
# git add --renormalize .
|
||||
*.onnx filter=lfs diff=lfs merge=lfs -text
|
||||
openpilot/selfdrive/modeld/models/big_driving_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
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -41,6 +41,7 @@ jobs:
|
||||
with:
|
||||
path: sunnypilot
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Get tinygrad_repo ref
|
||||
id: get-tinygrad-ref
|
||||
@@ -58,6 +59,7 @@ jobs:
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Get next JSON version to use (from GitHub docs repo)
|
||||
id: get-json
|
||||
|
||||
@@ -46,8 +46,8 @@ jobs:
|
||||
|
||||
if [ "${{ inputs.target }}" = "big" ]; then
|
||||
NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)")
|
||||
ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"
|
||||
HF_DEFAULTS_PATH="models/defaults/big"
|
||||
ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl"
|
||||
elif [ "${{ inputs.target }}" = "dm" ]; then
|
||||
ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx"
|
||||
HF_DEFAULTS_PATH="models/defaults/dm"
|
||||
@@ -57,8 +57,9 @@ jobs:
|
||||
ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx"
|
||||
HF_DEFAULTS_PATH="models/defaults/small"
|
||||
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)
|
||||
if [ -z "$TINYGRAD_REF" ]; then
|
||||
echo "::error::Failed to resolve tinygrad ref"
|
||||
@@ -78,6 +79,8 @@ jobs:
|
||||
if: ${{ inputs.target == 'small' }}
|
||||
runs-on: [self-hosted, tici]
|
||||
env:
|
||||
MODELS_DIR: openpilot/selfdrive/modeld/models
|
||||
COMPILER: tinygrad_repo/examples/openpilot
|
||||
SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx
|
||||
SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl
|
||||
steps:
|
||||
@@ -85,9 +88,10 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Pull ONNX via LFS
|
||||
- name: Pull small ONNX via LFS
|
||||
run: git lfs pull -I "${{ env.SMALL_ONNX }}"
|
||||
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
source /etc/profile
|
||||
@@ -103,38 +107,15 @@ jobs:
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
|
||||
|
||||
- name: Compile small model with stock compiler
|
||||
- name: Compile small model from ONNX
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
|
||||
|
||||
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)")
|
||||
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
|
||||
env ${TG_FLAGS} python3 \
|
||||
${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \
|
||||
--onnx ${{ github.workspace }}/${{ env.SMALL_ONNX }} \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
--frame-skip $FRAME_SKIP \
|
||||
--output ${{ github.workspace }}/${{ env.SMALL_PKL }}
|
||||
|
||||
- name: Chunk small pkl
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
python3 -c "
|
||||
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
|
||||
import os
|
||||
pkl = '${{ github.workspace }}/${{ env.SMALL_PKL }}'
|
||||
size = os.path.getsize(pkl)
|
||||
targets = get_chunk_targets(pkl, size)
|
||||
chunk_file(pkl, targets)
|
||||
print(f'Chunked into {len(targets)} files')
|
||||
"
|
||||
env ${TG_FLAGS} python3 ${{ github.workspace }}/${{ env.COMPILER }}/compile_onnx.py \
|
||||
${{ github.workspace }}/${{ env.SMALL_ONNX }} \
|
||||
${{ github.workspace }}/${{ env.SMALL_PKL }} \
|
||||
--device-input "*" --out-of-band --benchmark-runs 1
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
@@ -147,14 +128,16 @@ jobs:
|
||||
PKL_BASE="driving_tinygrad.pkl"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
|
||||
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
|
||||
cp "$MODELS_DIR/${PKL_BASE}" "$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" \
|
||||
--model-dir "$MODELS_DIR" \
|
||||
--output-dir "$OUTPUT_DIR" \
|
||||
--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"
|
||||
|
||||
@@ -181,15 +164,16 @@ jobs:
|
||||
if: ${{ inputs.target == 'big' }}
|
||||
runs-on: [self-hosted, chestnut]
|
||||
env:
|
||||
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
MODELS_DIR: openpilot/selfdrive/modeld/models
|
||||
COMPILER: tinygrad_repo/examples/openpilot
|
||||
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Pull big ONNX via LFS
|
||||
run: git lfs pull -I "${{ env.BIG_ONNX }}"
|
||||
- name: Pull big PKL via LFS
|
||||
run: git lfs pull -I "${{ env.BIG_PKL }}"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
@@ -222,38 +206,33 @@ jobs:
|
||||
raise RuntimeError('Chestnut PCIe link not ready after 10 attempts')
|
||||
"
|
||||
|
||||
- name: Compile big model with stock compiler
|
||||
- name: Compile big warp
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
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]}')")
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)")
|
||||
|
||||
TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
|
||||
|
||||
env ${TG_FLAGS} python3 \
|
||||
${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \
|
||||
--onnx ${{ github.workspace }}/${{ env.BIG_ONNX }} \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
--frame-skip $FRAME_SKIP \
|
||||
--output ${{ github.workspace }}/${{ env.BIG_PKL }}
|
||||
|
||||
- name: Chunk big pkl
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
python3 -c "
|
||||
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
|
||||
import os
|
||||
pkl = '${{ github.workspace }}/${{ env.BIG_PKL }}'
|
||||
size = os.path.getsize(pkl)
|
||||
targets = get_chunk_targets(pkl, size)
|
||||
chunk_file(pkl, targets)
|
||||
print(f'Chunked into {len(targets)} files')
|
||||
"
|
||||
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
|
||||
CAM_W=$(echo $res | cut -d x -f1)
|
||||
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}/big_driving_warp_${res}_tinygrad.pkl"
|
||||
env ${CHESTNUT_TG_FLAGS} python3 "${COMPILER}/compile_warp.py" \
|
||||
--frame ${CAM_W},${CAM_H},${STRIDE_INFO} \
|
||||
--warp-to ${MODEL_SIZE} \
|
||||
--layout yuv420 \
|
||||
--frames 2 \
|
||||
--output "${WARP_PKL}"
|
||||
done
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
@@ -266,14 +245,18 @@ jobs:
|
||||
PKL_BASE="big_driving_tinygrad.pkl"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
|
||||
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
|
||||
cp "$MODELS_DIR/${PKL_BASE}" "$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" \
|
||||
--model-dir "$MODELS_DIR" \
|
||||
--output-dir "$OUTPUT_DIR" \
|
||||
--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"
|
||||
|
||||
@@ -311,7 +294,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Pull ONNX via LFS
|
||||
- name: Pull LFS for resolved ONNX files
|
||||
if: ${{ needs.resolve.outputs.onnx_path != '' }}
|
||||
run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}"
|
||||
|
||||
- name: Install huggingface_hub
|
||||
@@ -376,38 +360,28 @@ jobs:
|
||||
from datetime import datetime, UTC
|
||||
|
||||
dm_dir = Path('dm_output')
|
||||
manifest = list(dm_dir.glob('*.chunkmanifest'))
|
||||
assert manifest, 'No chunkmanifest found'
|
||||
pkl_name = manifest[0].name.removesuffix('.chunkmanifest')
|
||||
num_chunks = int(manifest[0].read_text().strip())
|
||||
|
||||
chunks = []
|
||||
for i in range(num_chunks):
|
||||
chunk = dm_dir / f'{pkl_name}.chunk{i+1:02d}of{num_chunks:02d}'
|
||||
chunks.append({
|
||||
'file_name': chunk.name,
|
||||
'sha256': hashlib.sha256(chunk.read_bytes()).hexdigest()
|
||||
})
|
||||
matches = sorted(dm_dir.glob('dmonitoring_*_tinygrad.pkl'))
|
||||
assert len(matches) == 1, f'expected one DM pkl in {dm_dir}, found {matches}'
|
||||
pkl_name = matches[0].name
|
||||
|
||||
digest = hashlib.sha256()
|
||||
for c in chunks:
|
||||
with open(dm_dir / c['file_name'], 'rb') as f:
|
||||
while block := f.read(1024*1024):
|
||||
digest.update(block)
|
||||
with open(dm_dir / pkl_name, 'rb') as f:
|
||||
while block := f.read(1024*1024):
|
||||
digest.update(block)
|
||||
|
||||
metadata = {
|
||||
'bundles': [{
|
||||
'short_name': 'DMMODEL',
|
||||
'display_name': '${{ needs.resolve.outputs.model_name }}',
|
||||
'ref': '${{ needs.resolve.outputs.onnx_ref }}',
|
||||
'tinygrad_ref': '${{ needs.resolve.outputs.tinygrad_ref }}',
|
||||
'runner': 'tinygrad',
|
||||
'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||
'models': [{
|
||||
'type': 'chunked',
|
||||
'artifact': {
|
||||
'file_name': pkl_name,
|
||||
'download_uri': {'url': '', 'sha256': digest.hexdigest()},
|
||||
'chunks': chunks
|
||||
'download_uri': {'url': '', 'sha256': digest.hexdigest()}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
@@ -470,42 +444,39 @@ jobs:
|
||||
${{ github.workspace }}/${{ env.DM_ONNX }} \
|
||||
${{ github.workspace }}/${{ env.DM_PKL }}
|
||||
|
||||
- name: Chunk DM pkl
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
python3 -c "
|
||||
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
|
||||
import os
|
||||
pkl = '${{ github.workspace }}/${{ env.DM_PKL }}'
|
||||
size = os.path.getsize(pkl)
|
||||
targets = get_chunk_targets(pkl, size)
|
||||
chunk_file(pkl, targets)
|
||||
print(f'Chunked {pkl} into {len(targets)} chunks')
|
||||
"
|
||||
|
||||
- name: Compile DM warp
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
|
||||
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
MODEL_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld"
|
||||
COMPILER="${{ github.workspace }}/tinygrad_repo/examples/openpilot"
|
||||
MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models"
|
||||
DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
|
||||
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
|
||||
WARP_PKL="${MODEL_DIR}/models/dm_warp_${res}_tinygrad.pkl"
|
||||
taskset -c 7 env ${TG_FLAGS} python3 ${MODEL_DIR}/compile_dm_warp.py \
|
||||
--camera-resolution ${res} \
|
||||
CAM_W=$(echo $res | cut -d x -f1)
|
||||
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} \
|
||||
--output ${WARP_PKL}
|
||||
--layout luma \
|
||||
--border-fill 16 \
|
||||
--transform-device NPY \
|
||||
--output "${WARP_PKL}"
|
||||
done
|
||||
|
||||
- name: Prepare DM output
|
||||
run: |
|
||||
mkdir -p dm_output
|
||||
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
|
||||
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
|
||||
cp ${{ github.workspace }}/${{ env.DM_PKL }} dm_output/
|
||||
cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/
|
||||
|
||||
- name: Upload DM artifact
|
||||
@@ -519,4 +490,3 @@ jobs:
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
|
||||
|
||||
|
||||
@@ -134,12 +134,10 @@ jobs:
|
||||
|
||||
publish_model:
|
||||
if: ${{ !cancelled() && needs.build_model.result == 'success' }}
|
||||
concurrency:
|
||||
group: hf-push-${{ inputs.recompiled_dir }}
|
||||
cancel-in-progress: false
|
||||
needs: build_model
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
@@ -182,18 +180,21 @@ jobs:
|
||||
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download and extract model artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
||||
path: output
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
||||
run: |
|
||||
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
|
||||
env:
|
||||
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
||||
run: |
|
||||
mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}"
|
||||
cp -r output/* "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/"
|
||||
rm -f "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/artifact_name.txt"
|
||||
cp output/metadata.json "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/"
|
||||
|
||||
- name: Upload to Hugging Face
|
||||
env:
|
||||
@@ -205,13 +206,12 @@ jobs:
|
||||
"models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \
|
||||
--repo-type=dataset
|
||||
|
||||
- name: Pull gh-pages
|
||||
- name: Update and push JSON to docs repo
|
||||
run: |
|
||||
cd docs
|
||||
git pull origin gh-pages
|
||||
git config --global user.name "GitHub Action"
|
||||
git config --global user.email "action@github.com"
|
||||
|
||||
- name: Run json_parser.py to update JSON
|
||||
run: |
|
||||
FOLDER="${{ inputs.model_folder }}"
|
||||
if [ "$FOLDER" = "Other" ]; then
|
||||
FOLDER="${{ inputs.custom_model_folder }}"
|
||||
@@ -224,18 +224,27 @@ jobs:
|
||||
[ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\""
|
||||
[ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\""
|
||||
[ -n "${{ inputs.tinygrad_ref }}" ] && ARGS="$ARGS --tinygrad-ref \"${{ inputs.tinygrad_ref }}\""
|
||||
eval python3 docs/json_parser.py \
|
||||
--json-path "$JSON_FILE" \
|
||||
--recompiled-dir "local_models/$RECOMPILED_DIR" \
|
||||
--sort-by-date \
|
||||
$ARGS
|
||||
|
||||
- name: Push updated JSON to GitHub docs repo
|
||||
run: |
|
||||
cd docs
|
||||
git config --global user.name "GitHub Action"
|
||||
git config --global user.email "action@github.com"
|
||||
git checkout gh-pages
|
||||
git add docs/"$(basename $JSON_FILE)"
|
||||
git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit"
|
||||
git push origin gh-pages
|
||||
for attempt in $(seq 1 10); do
|
||||
git fetch origin gh-pages
|
||||
git reset --hard origin/gh-pages
|
||||
|
||||
eval python3 json_parser.py \
|
||||
--json-path "docs/$(basename $JSON_FILE)" \
|
||||
--recompiled-dir "../local_models/$RECOMPILED_DIR" \
|
||||
--sort-by-date \
|
||||
$ARGS
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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
|
||||
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
|
||||
@@ -18,24 +18,25 @@ concurrency:
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: build docs
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ${{
|
||||
(github.repository == 'commaai/openpilot') &&
|
||||
((github.event_name != 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
||||
&& fromJSON('["namespace-profile-amd64-8x16"]')
|
||||
|| fromJSON('["ubuntu-24.04"]') }}
|
||||
steps:
|
||||
- uses: commaai/timeout@v1
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
- run: ./tools/op.sh setup
|
||||
|
||||
# Build
|
||||
- name: Build docs
|
||||
run: |
|
||||
git lfs pull
|
||||
python docs/serve.py --build
|
||||
run: ./tools/op.sh docs --build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
+34
-18
@@ -1,5 +1,5 @@
|
||||
name: Download HF model chunks
|
||||
description: Resolve and download model chunks from HuggingFace in parallel
|
||||
name: Download HF model
|
||||
description: Resolve and download models from HuggingFace in parallel
|
||||
|
||||
inputs:
|
||||
hf_repo:
|
||||
@@ -11,16 +11,21 @@ inputs:
|
||||
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 model chunks
|
||||
- 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)
|
||||
@@ -28,30 +33,41 @@ runs:
|
||||
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
|
||||
local DEFAULTS BUNDLE ARTIFACT FILE_URL BASE_URL
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL")
|
||||
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)')
|
||||
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')
|
||||
BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||')
|
||||
NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length')
|
||||
FILE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url')
|
||||
BASE_URL=$(echo "$FILE_URL" | sed 's|/[^/]*$||')
|
||||
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -63,7 +79,7 @@ runs:
|
||||
done
|
||||
|
||||
TOTAL=$(wc -l < "$DOWNLOAD_LIST")
|
||||
echo "Downloading $TOTAL chunks with 8 parallel connections..."
|
||||
echo "Downloading $TOTAL files with 8 parallel connections..."
|
||||
xargs -P8 -d'\n' -I{} bash -c '
|
||||
URL="${1%% *}"
|
||||
DEST="${1#* }"
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Sync comma's LFS
|
||||
|
||||
env:
|
||||
LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs'
|
||||
LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git'
|
||||
LFS_URL: 'https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs'
|
||||
LFS_PUSH_URL: 'https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -40,14 +40,12 @@ jobs:
|
||||
git config --global user.name 'GitHub Action'
|
||||
git config --global user.email 'action@github.com'
|
||||
|
||||
- name: Set up SSH
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Add GitLab public keys
|
||||
- name: Setup hf
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
git config --global credential.helper store
|
||||
echo "https://user:${HF_TOKEN}@huggingface.co" > ~/.git-credentials
|
||||
|
||||
- name: Ensure branch
|
||||
run: |
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
|
||||
jobs:
|
||||
build___nightly:
|
||||
@@ -34,3 +34,7 @@ jobs:
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Push __nightly
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@ env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
|
||||
jobs:
|
||||
package_updates:
|
||||
|
||||
@@ -100,24 +100,56 @@ jobs:
|
||||
commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y')
|
||||
echo "model_date=${commit_date}" >> $GITHUB_OUTPUT
|
||||
cat $GITHUB_OUTPUT
|
||||
- run: |
|
||||
|
||||
- name: Fetch Models
|
||||
run: |
|
||||
cd ${{ github.workspace }}/openpilot/openpilot
|
||||
fetch_lfs() {
|
||||
sudo apt-get install -y -qq aria2 > /dev/null # Parallel ranges because gh is flaky
|
||||
LFS_URL=$(git config -f "$(git rev-parse --show-toplevel)/.lfsconfig" lfs.url)
|
||||
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"
|
||||
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
|
||||
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
|
||||
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
|
||||
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
|
||||
echo "::error::no big model ONNX or precompiled big_driving_tinygrad.pkl at this ref"
|
||||
exit 1
|
||||
fi
|
||||
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
|
||||
echo "::error::the ONNX files above are still LFS pointers, not real models"
|
||||
|
||||
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 or PKL files above are still LFS pointers, not real models"
|
||||
exit 1
|
||||
fi
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
|
||||
compression-level: 0
|
||||
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
|
||||
|
||||
build_model:
|
||||
@@ -132,7 +164,6 @@ jobs:
|
||||
fetch-depth: 1
|
||||
submodules: recursive
|
||||
|
||||
|
||||
- name: Set environment variables
|
||||
id: set-env
|
||||
run: |
|
||||
@@ -144,9 +175,6 @@ jobs:
|
||||
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
|
||||
uv sync --frozen
|
||||
printenv >> $GITHUB_ENV
|
||||
if [[ "${{ runner.debug }}" == "1" ]]; then
|
||||
cat $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
@@ -158,10 +186,6 @@ jobs:
|
||||
echo "VERSION: ${{ steps.set-env.outputs.version }}"
|
||||
echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}"
|
||||
echo "VIRTUAL_ENV: ${VIRTUAL_ENV}"
|
||||
echo "-------"
|
||||
if [[ "${{ runner.debug }}" == "1" ]]; then
|
||||
printenv
|
||||
fi
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
|
||||
rm -rf ${{ env.MODELS_DIR }}/*.onnx*
|
||||
@@ -184,11 +208,11 @@ jobs:
|
||||
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
|
||||
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 OPENPILOT_HACKS=1"
|
||||
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
|
||||
echo "CHESTNUT build"
|
||||
export CHESTNUT=1
|
||||
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1"
|
||||
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||
else
|
||||
echo "QCOM build"
|
||||
@@ -196,64 +220,77 @@ jobs:
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
||||
fi
|
||||
|
||||
# Generate metadata for all ONNX files
|
||||
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
|
||||
echo "Generating metadata: $onnx_file"
|
||||
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
||||
done
|
||||
NATIVE_PKL=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "*.pkl" -print -quit)
|
||||
|
||||
# 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
|
||||
if [ -f "${{ env.MODELS_DIR }}/upstream_pkl.txt" ]; then
|
||||
read -r TG_URL TG_SHA _ < "${{ env.MODELS_DIR }}/upstream_pkl.txt"
|
||||
echo "Converting commaai's precompiled model with tinygrad $TG_SHA"
|
||||
UPSTREAM_TG="${{ runner.temp }}/upstream_tinygrad"
|
||||
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
|
||||
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=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
|
||||
[ -f "$f" ] && POLICY_ONNX="$f" && break
|
||||
done
|
||||
POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
|
||||
[ -f "$f" ] && POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
OFF_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
|
||||
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
|
||||
done
|
||||
OFF_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
|
||||
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
ON_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
|
||||
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
|
||||
done
|
||||
ON_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
|
||||
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
SUPERCOMBO_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
|
||||
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
|
||||
done
|
||||
SUPERCOMBO_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
|
||||
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
|
||||
done
|
||||
|
||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||
if [ -f "$VISION_ONNX" ]; then
|
||||
ONNX_ARGS="--vision-onnx $VISION_ONNX"
|
||||
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
|
||||
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
|
||||
elif [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
|
||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||
if [ -f "$VISION_ONNX" ]; then
|
||||
ONNX_ARGS="--vision-onnx $VISION_ONNX"
|
||||
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
|
||||
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
|
||||
elif [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
|
||||
fi
|
||||
elif [ -f "$SUPERCOMBO_ONNX" ]; then
|
||||
MODEL_TYPE=supercombo
|
||||
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
|
||||
fi
|
||||
elif [ -f "$SUPERCOMBO_ONNX" ]; then
|
||||
MODEL_TYPE=supercombo
|
||||
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
|
||||
fi
|
||||
|
||||
if [ -n "$MODEL_TYPE" ]; then
|
||||
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
|
||||
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
|
||||
--model-type $MODEL_TYPE \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
$ONNX_ARGS \
|
||||
--output "$OUTPUT_PKL"
|
||||
if [ -n "$MODEL_TYPE" ]; then
|
||||
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
|
||||
env ${TG_FLAGS} taskset -c 7 python3 "$COMPILE_MODELD" \
|
||||
--model-type $MODEL_TYPE \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
$ONNX_ARGS \
|
||||
--output "$OUTPUT_PKL"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Prepare Output
|
||||
@@ -264,8 +301,7 @@ jobs:
|
||||
# Copy the model files
|
||||
rsync -avm \
|
||||
--include='*.dlc' \
|
||||
--include='*.chunk*' \
|
||||
--include='*.chunkmanifest' \
|
||||
--include='/${{ inputs.target_hardware == 'chestnut' && 'big_' || '' }}driving_tinygrad.pkl' \
|
||||
--exclude='*' \
|
||||
--delete-excluded \
|
||||
--chown=comma:comma \
|
||||
@@ -276,6 +312,7 @@ jobs:
|
||||
--output-dir "${{ env.OUTPUT_DIR }}" \
|
||||
--custom-name "${{ env.MODEL_NAME }}" \
|
||||
--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' || '' }}
|
||||
|
||||
- name: Write artifact name to file
|
||||
@@ -287,6 +324,7 @@ jobs:
|
||||
with:
|
||||
name: model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }}
|
||||
path: ${{ env.OUTPUT_DIR }}
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload artifact name file
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -170,6 +170,9 @@ jobs:
|
||||
scons -j1 cache_dir="$SCONS_CACHE" --minimal \
|
||||
openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd
|
||||
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
|
||||
touch ${BUILD_DIR}/prebuilt
|
||||
if [[ "${{ runner.debug }}" == "1" ]]; then
|
||||
@@ -221,37 +224,40 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
|
||||
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||
HF_DEFAULTS_PATH: models/defaults/big
|
||||
steps:
|
||||
- name: Resolve ONNX hash and tinygrad ref via API
|
||||
- name: Resolve tinygrad ref via API
|
||||
id: resolve
|
||||
run: |
|
||||
REF="${{ github.head_ref || github.ref_name }}"
|
||||
|
||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha')
|
||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl?ref=${REF}" --jq '.sha')
|
||||
[ -n "$BLOB_SHA" ] || { echo "::error::Failed to resolve big_driving_tinygrad.pkl blob SHA"; exit 1; }
|
||||
ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "ONNX hash: $ONNX_HASH"
|
||||
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract ONNX hash"; exit 1; }
|
||||
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')
|
||||
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"
|
||||
|
||||
check_defaults() {
|
||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" --arg ref "$TINYGRAD_REF" '
|
||||
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $ref or (.tinygrad_ref == null and $root.tinygrad_ref == $ref)))] | .[0] // empty
|
||||
' 2>/dev/null)
|
||||
[ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ]
|
||||
}
|
||||
|
||||
if check_defaults; then
|
||||
echo "HF defaults match repo ONNX hash and tinygrad ref"
|
||||
echo "HF defaults match repo tinygrad ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -304,12 +310,13 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
|
||||
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||
HF_DEFAULTS_PATH: models/defaults/small
|
||||
steps:
|
||||
- name: Resolve ONNX hash and tinygrad ref via API
|
||||
- name: Resolve tinygrad ref via API
|
||||
id: resolve
|
||||
run: |
|
||||
REF="${{ github.head_ref || github.ref_name }}"
|
||||
@@ -318,23 +325,24 @@ jobs:
|
||||
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"
|
||||
[ -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')
|
||||
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"
|
||||
|
||||
check_defaults() {
|
||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
[ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] || return 1
|
||||
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" --arg ref "$TINYGRAD_REF" '
|
||||
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $ref or (.tinygrad_ref == null and $root.tinygrad_ref == $ref)))] | .[0] // empty
|
||||
' 2>/dev/null)
|
||||
[ -n "$DRIVING" ] && [ "$DRIVING" != "null" ]
|
||||
}
|
||||
|
||||
if check_defaults; then
|
||||
echo "HF defaults match repo ONNX hash and tinygrad ref"
|
||||
echo "HF defaults match repo tinygrad ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -387,12 +395,13 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
|
||||
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||
HF_DEFAULTS_PATH: models/defaults/dm
|
||||
steps:
|
||||
- name: Resolve ONNX hash and tinygrad ref via API
|
||||
- name: Resolve tinygrad ref via API
|
||||
id: resolve
|
||||
run: |
|
||||
REF="${{ github.head_ref || github.ref_name }}"
|
||||
@@ -401,23 +410,24 @@ jobs:
|
||||
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"
|
||||
[ -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')
|
||||
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"
|
||||
|
||||
check_defaults() {
|
||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
[ -n "$DM" ] && [ "$DM" != "null" ] || return 1
|
||||
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" --arg ref "$TINYGRAD_REF" '
|
||||
. as $root | [.bundles[] | select(.onnx_sha256 == $hash and (.tinygrad_ref == $ref or (.tinygrad_ref == null and $root.tinygrad_ref == $ref)))] | .[0] // empty
|
||||
' 2>/dev/null)
|
||||
[ -n "$DM" ] && [ "$DM" != "null" ]
|
||||
}
|
||||
|
||||
if check_defaults; then
|
||||
echo "HF defaults match DM ONNX hash and tinygrad ref"
|
||||
echo "HF defaults match DM tinygrad ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -493,11 +503,12 @@ jobs:
|
||||
mkdir -p ${{ env.OUTPUT_DIR }}
|
||||
tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }}
|
||||
|
||||
- name: Download model chunks from HF
|
||||
uses: ./.github/workflows/download-hf-model-chunks
|
||||
- name: Download models from HF
|
||||
uses: ./.github/workflows/download-hf-model
|
||||
with:
|
||||
hf_repo: sunnypilot/sunnypilot_models_v1
|
||||
dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models
|
||||
tinygrad_ref: ${{ needs.prepare_dm_model.outputs.tinygrad_ref }}
|
||||
models: |
|
||||
[
|
||||
{"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"},
|
||||
@@ -562,11 +573,12 @@ jobs:
|
||||
mkdir -p ${{ env.OUTPUT_DIR }}
|
||||
tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }}
|
||||
|
||||
- name: Download model chunks from HF
|
||||
uses: ./.github/workflows/download-hf-model-chunks
|
||||
- name: Download models from HF
|
||||
uses: ./.github/workflows/download-hf-model
|
||||
with:
|
||||
hf_repo: sunnypilot/sunnypilot_models_v1
|
||||
dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models
|
||||
tinygrad_ref: ${{ needs.prepare_dm_model.outputs.tinygrad_ref }}
|
||||
models: |
|
||||
[
|
||||
{"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"},
|
||||
|
||||
@@ -47,33 +47,23 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Download Model Chunks in Parallel
|
||||
- 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/model_chunks
|
||||
echo '${{ toJson(matrix.artifact.chunks) }}' > chunks.json
|
||||
|
||||
BASE_URL="${{ matrix.artifact.download_uri.url }}"
|
||||
export BASE_DIR=$(dirname "$BASE_URL")
|
||||
|
||||
python3 -c '
|
||||
import json, os
|
||||
with open("chunks.json") as f:
|
||||
chunks = json.load(f)
|
||||
manifest_path = f"/tmp/model_chunks/${{ matrix.artifact.file_name }}.chunkmanifest"
|
||||
with open(manifest_path, "w") as f:
|
||||
f.write(str(len(chunks)))
|
||||
base_dir = os.environ["BASE_DIR"]
|
||||
with open("/tmp/curl_config.txt", "w") as f:
|
||||
for c in chunks:
|
||||
fn = c["file_name"]
|
||||
f.write(f"url = \"{base_dir}/{fn}\"\noutput = \"/tmp/model_chunks/{fn}\"\n")
|
||||
'
|
||||
curl -Z --parallel-immediate --parallel-max 16 -s -S -f -L -K /tmp/curl_config.txt
|
||||
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_CHUNK_DIR: "/tmp/model_chunks"
|
||||
PYTHONPATH: ".:./tinygrad_repo"
|
||||
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
|
||||
|
||||
@@ -22,7 +22,7 @@ env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
|
||||
jobs:
|
||||
build_release:
|
||||
|
||||
@@ -50,6 +50,8 @@ 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
|
||||
*.pkl
|
||||
*.pkl*
|
||||
!openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
!openpilot/sunnypilot/modeld_v2/models/*.pkl
|
||||
config.json
|
||||
compile_commands.json
|
||||
compare_runtime*.html
|
||||
|
||||
@@ -19,3 +19,4 @@
|
||||
[submodule "sunnypilot/neural_network_data"]
|
||||
path = openpilot/sunnypilot/neural_network_data
|
||||
url = https://github.com/sunnypilot/neural-network-data.git
|
||||
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
[lfs]
|
||||
url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs
|
||||
pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git
|
||||
url = https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs
|
||||
pushurl = https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs
|
||||
|
||||
locksverify = false
|
||||
|
||||
Vendored
+62
-4
@@ -12,16 +12,17 @@ def retryWithDelay(int maxRetries, int delay, Closure body) {
|
||||
def device(String ip, String step_label, String cmd) {
|
||||
withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) {
|
||||
def ssh_cmd = """
|
||||
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'
|
||||
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'
|
||||
|
||||
set -e
|
||||
|
||||
export TERM=xterm-256color
|
||||
|
||||
shopt -s huponexit # kill all child processes when the shell exits
|
||||
trap 'kill 0' HUP # stop this process group on SSH disconnect
|
||||
|
||||
export CI=1
|
||||
export PYTHONWARNINGS=error
|
||||
export PYTHONFAULTHANDLER=1
|
||||
export COMMA_CACHE=/data/tmp/comma_download_cache
|
||||
#export LOGPRINT=debug # this has gotten too spammy...
|
||||
export TEST_DIR=${env.TEST_DIR}
|
||||
@@ -69,7 +70,8 @@ export LD_LIBRARY_PATH="\$(python -c 'import ffmpeg; print(ffmpeg.LIB_DIR)'):/us
|
||||
ln -snf ${env.TEST_DIR} /data/pythonpath
|
||||
|
||||
cd ${env.TEST_DIR} || true
|
||||
time ${cmd}
|
||||
time ( ${cmd} ) &
|
||||
wait \$!
|
||||
END"""
|
||||
|
||||
sh script: ssh_cmd, label: step_label
|
||||
@@ -159,6 +161,14 @@ def step(String name, String cmd, Map 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 {
|
||||
env.CI = "1"
|
||||
env.PYTHONWARNINGS = "error"
|
||||
@@ -169,7 +179,37 @@ node {
|
||||
env.GIT_BRANCH = checkout(scm).GIT_BRANCH
|
||||
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
|
||||
|
||||
def excludeBranches = ['__nightly', 'devel', 'devel-staging',
|
||||
if (env.JOB_BASE_NAME == 'chestnut-stresstest') {
|
||||
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-*']
|
||||
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
|
||||
|
||||
@@ -201,6 +241,21 @@ 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)) {
|
||||
parallel (
|
||||
'onroad tests': {
|
||||
@@ -252,6 +307,9 @@ node {
|
||||
step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"),
|
||||
])
|
||||
},
|
||||
'chestnut': {
|
||||
chestnutStage()
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,4 +1,14 @@
|
||||
Version 0.11.2 (2026-08-12)
|
||||
Version 0.11.3 (2026-XX-XX)
|
||||
========================
|
||||
* 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
|
||||
* Big model with 880M parameters
|
||||
|
||||
+2
-13
@@ -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('--verbose', action='store_true', default=False, help='show full build commands')
|
||||
release = not os.path.exists(File('#.gitattributes').abspath) # file absent on release branch, see release_files.py
|
||||
release = not os.path.exists(File('#.gitmodules').abspath) # file absent on release branch, see release_files.py
|
||||
AddOption('--minimal',
|
||||
action='store_false',
|
||||
dest='extras',
|
||||
@@ -87,7 +87,6 @@ acados_include_dirs = [
|
||||
# vendored in commaai/dependencies.
|
||||
allowed_system_libs = {
|
||||
"EGL", "GLESv2", "GL",
|
||||
"Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
|
||||
"dl", "drm", "gbm", "m", "pthread",
|
||||
}
|
||||
|
||||
@@ -121,6 +120,7 @@ def _libflags(target, source, env, for_signature):
|
||||
env = Environment(
|
||||
ENV={
|
||||
"PATH": os.environ['PATH'],
|
||||
"TMPDIR": os.environ.get('TMPDIR', '/tmp'),
|
||||
"PYTHONPATH": os.pathsep.join(submodule_python_paths),
|
||||
"ACADOS_SOURCE_DIR": acados.DIR,
|
||||
"ACADOS_PYTHON_INTERFACE_PATH": acados.TEMPLATE_DIR,
|
||||
@@ -316,8 +316,6 @@ def count_scons_nodes(nodes):
|
||||
if node in seen:
|
||||
continue
|
||||
seen.add(node)
|
||||
if hasattr(node, 'has_builder') and node.has_builder():
|
||||
build_product_nodes.add(node)
|
||||
executor = node.get_executor()
|
||||
if executor is not None:
|
||||
stack += executor.get_all_prerequisites() + executor.get_all_children()
|
||||
@@ -326,7 +324,6 @@ def count_scons_nodes(nodes):
|
||||
|
||||
progress_interval = 5
|
||||
progress_count = 0
|
||||
build_product_nodes = set()
|
||||
progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry)))
|
||||
|
||||
def progress_function(node):
|
||||
@@ -342,11 +339,3 @@ def progress_function(node):
|
||||
|
||||
Progress(progress_function, interval=progress_interval)
|
||||
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
@@ -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|[](##)|[](##)|<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|[](##)|[](##)|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|[](##)|[](##)|<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)|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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>|||
|
||||
|CUPRA|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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>|||
|
||||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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-25|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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|HR-V 2023-27|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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|Insight 2019-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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>|||
|
||||
|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|[](##)|[](##)|<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 HW4) 2023-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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>|||
|
||||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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)|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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[<sup>12</sup>](#footnotes)|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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>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>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>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>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>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
@@ -5,10 +5,10 @@ The site is updated on pushes to master by this [workflow](../.github/workflows/
|
||||
|
||||
**1. Build the site**
|
||||
``` bash
|
||||
python docs/serve.py --build
|
||||
op docs --build
|
||||
```
|
||||
|
||||
**2. Run the site locally** (rebuilds on change)
|
||||
``` bash
|
||||
python docs/serve.py
|
||||
op docs
|
||||
```
|
||||
|
||||
+12
-10
@@ -18,21 +18,21 @@ function agnos_init {
|
||||
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
|
||||
|
||||
# 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"
|
||||
MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json"
|
||||
if $AGNOS_PY --verify $MANIFEST; then
|
||||
if "$AGNOS_PY" --verify "$MANIFEST"; then
|
||||
sudo reboot
|
||||
fi
|
||||
while true; do
|
||||
$DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
|
||||
"$DIR/openpilot/common/hardware/comma/updater" "$AGNOS_PY" "$MANIFEST"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
function launch {
|
||||
# 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
|
||||
# are as follows:
|
||||
@@ -44,7 +44,7 @@ function launch {
|
||||
# that completed successfully and synced to disk.
|
||||
|
||||
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
|
||||
echo "${DIR} has been modified, skipping overlay update installation"
|
||||
else
|
||||
@@ -53,9 +53,11 @@ function launch {
|
||||
echo "Valid overlay update found, installing"
|
||||
LAUNCHER_LOCATION="${BASH_SOURCE[0]}"
|
||||
|
||||
mv $DIR /data/safe_staging/old_openpilot
|
||||
mv "${STAGING_ROOT}/finalized" $DIR
|
||||
cd $DIR
|
||||
mv "$DIR" /data/safe_staging/old_openpilot
|
||||
mv "${STAGING_ROOT}/finalized" "$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}"
|
||||
unset AGNOS_VERSION
|
||||
@@ -69,7 +71,7 @@ function launch {
|
||||
fi
|
||||
|
||||
# handle pythonpath
|
||||
ln -sfn $(pwd) /data/pythonpath
|
||||
ln -sfn "$(pwd)" /data/pythonpath
|
||||
export PYTHONPATH="$PWD"
|
||||
|
||||
# submodule package symlinks for PYTHONPATH imports on device.
|
||||
@@ -90,7 +92,7 @@ function launch {
|
||||
|
||||
# start manager
|
||||
cd openpilot/system/manager
|
||||
if [ ! -f $DIR/prebuilt ]; then
|
||||
if [ ! -f "$DIR/prebuilt" ]; then
|
||||
./build.py
|
||||
fi
|
||||
./manager.py
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="19.7"
|
||||
export AGNOS_VERSION="19.9"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: f95f996f59...e183d91f99
@@ -196,7 +196,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
}
|
||||
|
||||
struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
dec @0 :DynamicExperimentalControl;
|
||||
longitudinalPlanSource @1 :LongitudinalPlanSource;
|
||||
smartCruiseControl @2 :SmartCruiseControl;
|
||||
speedLimit @3 :SpeedLimit;
|
||||
@@ -205,7 +204,11 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
deprecated :group {
|
||||
dec @0 :DynamicExperimentalControlDEPRECATED;
|
||||
}
|
||||
|
||||
struct DynamicExperimentalControlDEPRECATED {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
enabled @1 :Bool;
|
||||
active @2 :Bool;
|
||||
|
||||
+22
-16
@@ -128,6 +128,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
||||
personalityChanged @91;
|
||||
aeb @92;
|
||||
userBookmark @95;
|
||||
userBookmarkNotPaired @104;
|
||||
excessiveActuation @96;
|
||||
bigModelLoading @100;
|
||||
bigModelFailed @102;
|
||||
@@ -166,12 +167,7 @@ struct InitData {
|
||||
gitSrcCommit @23 :Text;
|
||||
gitSrcCommitDate @24 :Text;
|
||||
|
||||
androidProperties @16 :Map(Text, Text);
|
||||
|
||||
pandaInfo @8 :PandaInfo;
|
||||
|
||||
dirty @9 :Bool;
|
||||
passive @12 :Bool;
|
||||
params @17 :Map(Text, Data);
|
||||
|
||||
commands @19 :Map(Text, Data);
|
||||
@@ -189,6 +185,17 @@ struct InitData {
|
||||
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 {
|
||||
hasPanda @0 :Bool;
|
||||
dongleId @1 :Text;
|
||||
@@ -199,14 +206,6 @@ struct InitData {
|
||||
struct ChffrAndroidExtra {
|
||||
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 {
|
||||
@@ -572,6 +571,9 @@ struct PandaState @0xa7649e2575e4591e {
|
||||
controlsAllowedLateral @38 :Bool;
|
||||
controlsAllowedLongitudinal @39 :Bool;
|
||||
|
||||
nmiReset @40 :Bool;
|
||||
hardfaultReset @41 :Bool;
|
||||
|
||||
enum FaultStatus {
|
||||
none @0;
|
||||
faultTemp @1;
|
||||
@@ -694,6 +696,7 @@ struct PeripheralState {
|
||||
|
||||
struct UsbState {
|
||||
devices @0 :List(Device);
|
||||
connected @1 :Bool;
|
||||
|
||||
struct Device {
|
||||
busnum @0 :UInt8;
|
||||
@@ -819,7 +822,6 @@ struct SelfdriveState {
|
||||
alertHudVisual @12 :Car.CarControl.HUDControl.VisualAlert;
|
||||
|
||||
# configurable driving settings
|
||||
experimentalMode @10 :Bool;
|
||||
personality @11 :LongitudinalPersonality;
|
||||
|
||||
enum AudibleAlert {
|
||||
@@ -863,6 +865,7 @@ struct SelfdriveState {
|
||||
|
||||
deprecated :group {
|
||||
alertSound @8 :Car.CarControl.HUDControl.AudibleAlert;
|
||||
experimentalMode @10 :Bool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1237,6 +1240,7 @@ struct LongitudinalPlan @0xe00b5b3eba12876c {
|
||||
shouldStop @37: Bool;
|
||||
allowThrottle @38: Bool;
|
||||
allowBrake @39: Bool;
|
||||
accelBoost @40 :Float32;
|
||||
|
||||
|
||||
solverExecutionTime @35 :Float32;
|
||||
@@ -2234,6 +2238,7 @@ struct DriverMonitoringState {
|
||||
pose @0: Bool;
|
||||
eye @1: Bool;
|
||||
phone @2: Bool;
|
||||
sleep @3: Bool;
|
||||
}
|
||||
|
||||
struct Pose {
|
||||
@@ -2594,6 +2599,7 @@ struct Event {
|
||||
clocks @35 :Clocks;
|
||||
deviceState @6 :DeviceState;
|
||||
chestnutState @152 :ChestnutState;
|
||||
chestnutGpuState @153 :ChestnutState;
|
||||
logMessage @18 :Text;
|
||||
errorLogMessage @85 :Text;
|
||||
|
||||
@@ -2704,8 +2710,8 @@ struct Event {
|
||||
driverMonitoringStateDEPRECATED @71 :DriverMonitoringStateDEPRECATED;
|
||||
gpsNMEADEPRECATED @3 :GPSNMEAData;
|
||||
uploaderStateDEPRECATED @79 :UploaderState;
|
||||
navInstructionDEPRECATED @82 :NavInstruction;
|
||||
navRouteDEPRECATED @83 :NavRoute;
|
||||
navInstruction @82 :NavInstruction;
|
||||
navRoute @83 :NavRoute;
|
||||
navThumbnailDEPRECATED @84 :Thumbnail;
|
||||
gnssMeasurementsDEPRECATED @91 :GnssMeasurements;
|
||||
mapRenderStateDEPRECATED @105: MapRenderState;
|
||||
|
||||
@@ -25,7 +25,8 @@ _services: dict[str, tuple] = {
|
||||
"accelerometer": (True, 104., 104),
|
||||
"temperatureSensor": (True, 2., 200),
|
||||
"deviceState": (True, 2., 1),
|
||||
"chestnutState": (True, 10., 10),
|
||||
"chestnutState": (True, 10., 1),
|
||||
"chestnutGpuState": (False, 10.),
|
||||
"touch": (True, 20., 1),
|
||||
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
|
||||
"controlsState": (True, 100., 10, QueueSize.MEDIUM),
|
||||
@@ -51,6 +52,8 @@ _services: dict[str, tuple] = {
|
||||
"procLog": (True, 0.5, 15, QueueSize.BIG),
|
||||
"gpsLocationExternal": (True, 10., 10),
|
||||
"gpsLocation": (True, 1., 1),
|
||||
"navInstruction": (True, 1., 10),
|
||||
"navRoute": (True, 0.),
|
||||
"ubloxGnss": (True, 10.),
|
||||
"qcomGnss": (True, 2.),
|
||||
"clocks": (True, 0.1, 1),
|
||||
|
||||
@@ -16,6 +16,10 @@ class CV:
|
||||
DEG_TO_RAD = np.pi / 180.
|
||||
RAD_TO_DEG = 1. / DEG_TO_RAD
|
||||
|
||||
# Length
|
||||
METER_TO_FOOT = 3.28084
|
||||
METER_TO_MILE = 1. / 1609.344
|
||||
|
||||
# Mass
|
||||
LB_TO_KG = 0.453592
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
WEBBING_ICCID_PREFIX = "8985235"
|
||||
|
||||
|
||||
class LPAError(RuntimeError):
|
||||
pass
|
||||
@@ -19,7 +21,14 @@ class Profile:
|
||||
|
||||
@property
|
||||
def is_comma(self) -> bool:
|
||||
return self.provider == 'Webbing' and self.iccid.startswith('8985235')
|
||||
return self.provider == 'Webbing' and self.iccid.startswith(WEBBING_ICCID_PREFIX)
|
||||
|
||||
@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):
|
||||
|
||||
@@ -613,7 +613,7 @@ def parse_lpa_activation_code(activation_code: str) -> tuple[str, str]:
|
||||
if not activation_code.startswith("LPA:"):
|
||||
raise ValueError("Invalid activation code format")
|
||||
parts = activation_code[4:].split("$")
|
||||
if len(parts) != 3:
|
||||
if len(parts) != 3 or not all(parts):
|
||||
raise ValueError("Invalid activation code format")
|
||||
return parts[1], parts[2]
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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}"
|
||||
Path(manifest_path).unlink(missing_ok=True)
|
||||
with open(path, 'rb') as f:
|
||||
for chunk_path in chunk_paths:
|
||||
with open(chunk_path, 'wb') as out:
|
||||
@@ -31,14 +32,6 @@ def chunk_file(path, targets):
|
||||
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)
|
||||
@@ -66,11 +59,11 @@ class ChunkStream(io.RawIOBase):
|
||||
|
||||
def open_file_chunked(path):
|
||||
manifest_path = get_manifest_path(path)
|
||||
if os.path.isfile(manifest_path):
|
||||
if os.path.isfile(path):
|
||||
paths = [path]
|
||||
elif 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))
|
||||
|
||||
@@ -27,16 +27,6 @@ def gpio_read(pin: int) -> bool | None:
|
||||
|
||||
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
|
||||
def get_irq_action(irq: int) -> list[str]:
|
||||
try:
|
||||
|
||||
@@ -145,6 +145,9 @@ class HardwareBase(ABC):
|
||||
def get_modem_temperatures(self):
|
||||
return []
|
||||
|
||||
def get_modem_state(self) -> dict:
|
||||
return {}
|
||||
|
||||
def initialize_hardware(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -56,28 +56,28 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
|
||||
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"size": 46897152,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615.img.xz",
|
||||
"hash": "b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615",
|
||||
"hash_raw": "b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615",
|
||||
"size": 46962688,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
|
||||
"ondevice_hash": "3ef4769bb1cc73bfc31f96f073b46af42dcc0bad4ab2dde32d11929745d0cd28"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
|
||||
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
|
||||
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7.img.xz",
|
||||
"hash": "9aebca09b9d142718e71efbfe44b723010eec6e70067c043c63186067291739d",
|
||||
"hash_raw": "80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
|
||||
"ondevice_hash": "270b76c5c5568828e3883390b83ea445425a104dd42119d68f06093ce6b6fcef",
|
||||
"alt": {
|
||||
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
|
||||
"hash": "80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from functools import cached_property, lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
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.gpio import gpio_set, gpio_init, get_irqs_for_action
|
||||
from openpilot.common.esim.base import LPABase
|
||||
@@ -207,6 +208,8 @@ class HardwareComma(HardwareBase):
|
||||
|
||||
def get_network_metered(self, network_type) -> bool:
|
||||
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
|
||||
return Params().get_bool("GsmMetered")
|
||||
try:
|
||||
|
||||
@@ -17,6 +17,8 @@ from ipaddress import IPv4Address, AddressValueError
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from openpilot.common.esim.base import WEBBING_ICCID_PREFIX
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s",
|
||||
@@ -44,7 +46,6 @@ NETWORK_TYPE = {0: "gsm", 1: "gsm", 3: "gsm", 8: "gsm",
|
||||
11: "nr", 12: "nr", 13: "nr"}
|
||||
|
||||
DIAL_CID = 1
|
||||
WEBBING_ICCID_PREFIX = "8985235"
|
||||
|
||||
PPPD_CMD = [
|
||||
"sudo", "pppd", PPP_PORT, "460800", "noauth", "nodetach", "noipdefault", "usepeerdns",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
|
||||
size 24709205
|
||||
oid sha256:f92c0a9b41613e063b6a1cd086729f5555d9df6482249e171ed1e276272cad7d
|
||||
size 24709520
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||
CHESTNUT_FW_VERSION = "8516b768"
|
||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
|
||||
@@ -36,6 +36,10 @@ def read_int(path: Path, base: int = 10) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cable_connected() -> bool:
|
||||
return read_int(TYPEC_CC_ORIENTATION_PATH) != 0
|
||||
|
||||
|
||||
def usb_devices() -> list[Path]:
|
||||
try:
|
||||
devices = (d for d in USB_DEVICES_PATH.glob("*") if (d / "idVendor").exists())
|
||||
@@ -91,3 +95,4 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
||||
chestnut_present = True
|
||||
|
||||
device_state.chestnutPresent = chestnut_present
|
||||
device_state.usbState.connected = cable_connected()
|
||||
|
||||
@@ -28,6 +28,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
{"CurrentBootlog", {PERSISTENT, 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}},
|
||||
{"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -138,7 +139,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"ChestnutModelError", {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}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
@@ -240,7 +249,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// sunnypilot model params
|
||||
|
||||
+556
-35
@@ -1,16 +1,47 @@
|
||||
"""Small QR encoder for the UI's byte-mode, error-correction-level-L codes."""
|
||||
"""QR code encoding, decoding, and UI textures."""
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
|
||||
# Indexes are QR versions. These are the only two Reed-Solomon parameters needed
|
||||
# for error-correction level L.
|
||||
_ECC_LEN = (0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28)
|
||||
_NUM_BLOCKS = (0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8)
|
||||
# (ec codewords per block, block count) for levels L, M, Q, H, versions 1-40
|
||||
_EC = [
|
||||
((7, 1), (10, 1), (13, 1), (17, 1)), ((10, 1), (16, 1), (22, 1), (28, 1)), ((15, 1), (26, 1), (18, 2), (22, 2)),
|
||||
((20, 1), (18, 2), (26, 2), (16, 4)), ((26, 1), (24, 2), (18, 4), (22, 4)), ((18, 2), (16, 4), (24, 4), (28, 4)),
|
||||
((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)),
|
||||
]
|
||||
|
||||
# 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412
|
||||
_FORMAT_BITS = 0b111011111000100
|
||||
# GF(256) with the QR polynomial x^8 + x^4 + x^3 + x^2 + 1: powers of alpha and their logs
|
||||
_EXP = [1]
|
||||
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:
|
||||
@@ -21,8 +52,24 @@ def _raw_modules(version: int) -> int:
|
||||
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:
|
||||
return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version]
|
||||
return sum(_block_lengths(version, 0))
|
||||
|
||||
|
||||
def _append_bits(bits: list[int], value: int, length: int) -> None:
|
||||
@@ -49,37 +96,18 @@ def _data_codewords(data: bytes, version: int) -> bytes:
|
||||
def _codewords(data: bytes, version: int) -> bytes:
|
||||
"""Split data codewords into Reed-Solomon blocks and interleave data + ECC."""
|
||||
data = _data_codewords(data, version)
|
||||
num_blocks = _NUM_BLOCKS[version]
|
||||
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]] = []
|
||||
divisor = _divisor(_EC[version - 1][0][0])
|
||||
blocks = []
|
||||
offset = 0
|
||||
for i in range(num_blocks):
|
||||
length = short_len - ecc_len + (0 if i < num_short else 1)
|
||||
for length in _block_lengths(version, 0):
|
||||
block = data[offset:offset + length]
|
||||
blocks.append((block, _remainder(block, divisor)))
|
||||
blocks.append(block + _remainder(block, divisor))
|
||||
offset += length
|
||||
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)
|
||||
return bytes(blocks[b][i] for b, i in _interleaved(version, 0))
|
||||
|
||||
|
||||
def _multiply(x: int, y: int) -> int:
|
||||
result = 0
|
||||
for _ in range(8):
|
||||
result = (result << 1) ^ (0x11D if result & 0x80 else 0)
|
||||
if y & 0x80:
|
||||
result ^= x
|
||||
y <<= 1
|
||||
return result
|
||||
return _EXP[(_LOG[x] + _LOG[y]) % 255] if x and y else 0
|
||||
|
||||
|
||||
def _divisor(degree: int) -> bytes:
|
||||
@@ -108,7 +136,7 @@ def _alignment_positions(version: int) -> list[int]:
|
||||
if version == 1:
|
||||
return []
|
||||
count = version // 7 + 2
|
||||
step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2
|
||||
step = (version * 8 + count * 3 + 5) // (count * 4 - 4) * 2
|
||||
return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1]
|
||||
|
||||
|
||||
@@ -171,7 +199,7 @@ class _Qr:
|
||||
|
||||
def _format(self) -> None:
|
||||
for i in range(15):
|
||||
bit = ((_FORMAT_BITS >> i) & 1) != 0
|
||||
bit = ((_FORMATS[1 << 3 | 0] >> i) & 1) != 0 # level L, mask 0
|
||||
y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i
|
||||
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
|
||||
@@ -216,3 +244,496 @@ def make_texture(data: str, inverted: bool = False) -> rl.Texture:
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
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
|
||||
|
||||
@@ -91,8 +91,13 @@ class Ratekeeper:
|
||||
self.avg_dt.add_value(self._last_monitor_time - prev)
|
||||
|
||||
lagged = False
|
||||
remaining = self._next_frame_time - time.monotonic()
|
||||
self._next_frame_time += self._interval
|
||||
now = time.monotonic()
|
||||
remaining = self._next_frame_time - now
|
||||
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:
|
||||
print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms")
|
||||
lagged = True
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,178 @@
|
||||
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"
|
||||
@@ -5,7 +5,6 @@ import contextlib
|
||||
import subprocess
|
||||
import time
|
||||
import functools
|
||||
from subprocess import Popen, PIPE, TimeoutExpired
|
||||
import zstandard as zstd
|
||||
|
||||
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
|
||||
@@ -157,20 +156,6 @@ def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> st
|
||||
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):
|
||||
rows = [list(row) for row in tabular_data]
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define COMMA_VERSION "0.11.2"
|
||||
#define COMMA_VERSION "0.11.3"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:911f59f248015600da7ecc689398103d47dfc57f6be17ac8c8e543a726a6c64b
|
||||
size 3311
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd5d8d3fce5e30662797f7eeed224f5775b2cabcc055f01f5ebf6e7b2657b616
|
||||
size 1801
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e58deb1778cf2826339f27e9f09eecc79ea137c1436210c14b71c352a649c77
|
||||
size 34953
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7ab989c9fe22e7d2119bac5284cdccfba2889477d69f5f6c6b4470c7ee0aab3
|
||||
size 1801
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f37a02dd914405c6f86f415700dd5985eb976b923e7abd6580d2da76533594e
|
||||
size 9466
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90d441310f8e1833c661d8a4f0546aab2eb50eb21cc21beb0aff0d27b5ee6066
|
||||
size 1571
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b2dad33fead9f064c3a548651d6ef37daf82b6127c329683f538ec6e986ecbc
|
||||
size 11204
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82648254da89edb0cf65ef63fc5e6e0741414051bfea8a40449132fd61b1ed0f
|
||||
size 1533
|
||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:01841b602632c66ab14a8e52b874a1623f09641dc2ef0620f4e2d00bb4a913f3
|
||||
size 16243
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0936a3d890a3ad68ffe021e4905b9b9e59ffc2ae19ee9c703daa3e7c92e7c78
|
||||
size 1793
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57c7ba72642b0350ee764c924586fa66ff94915be4aba1813797e67eb4b74376
|
||||
size 33215
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71dc069bd3dc5b11ba8e1b52eb3a1025ee0ad8c82834f94b4b2269ec0d401920
|
||||
size 237
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:73a6fe02cf19105c0051a6664a424c1bf2ede364c6cf3791f235c86ffa203f39
|
||||
size 10348
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4f500f118dca3eedbd86fc6cc943ad8be67fe5d4f7b474d5b452cd757ad805c3
|
||||
size 282
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a49996f0ce069a048abe94ba9424de655dcff53745676ab35e045204af88a3c
|
||||
size 12103
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8542374779f1908dc52c8a126bc041efea4c4d605afdd34e3bf8e520a7e78bf2
|
||||
size 320
|
||||
@@ -0,0 +1,3 @@
|
||||
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
|
||||
oid sha256:fd91685bf656e828648acf035a4737acb2c4709e8514cf0aa0a10fa470a9bb60
|
||||
size 11580
|
||||
oid sha256:92431d37a844206570d73d48f39dd8d934539a89668fe8d5f4cccae38f7b301b
|
||||
size 11530
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3efe7d12ae82e0f5a1bd773129cf1eafe3c8c36ff5cba43a6c95f295706062b5
|
||||
size 19724
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dfb645e59636687d61d20283bae65b68ddda6f7b2d4fda01a6e613a842e3b41f
|
||||
size 823
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df07383a6ddfe8cd7eeb2f22271cbd13068327ca0978873f8d10368860562e40
|
||||
size 111083
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7518f3409e2ff8f7e99fb7f0c06afa8bdf0b0a506147b5cf9d4931b820a1a75
|
||||
size 1107
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a18e3de5f455b4089a824da8d5d78d5d532ef71facb05cc75c178b24d5beeb7
|
||||
size 920
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:710e74b526adb5bbac15cd8915a828dd673dd8ce15b2fd694c64c182eef6f56f
|
||||
size 13744
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25db08a9038b5faa3b200ef5625cb216d7f4b31ea3d49fb0395f9b726bfaf8e6
|
||||
size 13135
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:547f466cc59d29647f3bf131d8444d7f22b63b186bcea55ceb8109c68ca74fe8
|
||||
size 2345
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55050096cd98e7f4ea3e843d927c18815e458dfbd5e7d100b503db92075d061b
|
||||
size 2373
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f9c439ed89bc0357e86c005d5dbb6f1ad2b8d72cdc0890e3201d842d8c3928f1
|
||||
size 2443
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7aeccd3e0d8517f74eb34f7d73632c0b29b0ad5df5b9686c0f509bb6b136561e
|
||||
size 1728
|
||||
@@ -23,8 +23,8 @@ done
|
||||
|
||||
# sudo apt install inkscape
|
||||
|
||||
for svg in $(find $DIR -type f | grep svg$); do
|
||||
bunx svgo $svg --multipass --pretty --indent 2
|
||||
for svg in $(find "$DIR" -type f | grep svg$); do
|
||||
bunx svgo "$svg" --multipass --pretty --indent 2
|
||||
|
||||
# convert to PNG
|
||||
png="${svg%.svg}.png"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d02369e21d3c78a3a5ece7e78deedb82343bc3228a28db73da48b5c27860004
|
||||
size 52374
|
||||
@@ -0,0 +1,3 @@
|
||||
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
|
||||
oid sha256:1bb440f424c989c06e203bfba0b32d39aede8f6c78a9b335e5e778460202b601
|
||||
oid sha256:884e828e5c51d980d41b6e2400fcb4f4a926275018a00674d8239184e1eb96db
|
||||
size 73026
|
||||
|
||||
@@ -127,9 +127,6 @@ class Car:
|
||||
set_alternative_experience(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")
|
||||
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
|
||||
@@ -181,14 +178,11 @@ class Car:
|
||||
self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP)
|
||||
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
||||
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True)
|
||||
|
||||
# card is driven by can recv, expected at 100Hz
|
||||
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]:
|
||||
"""carState update loop, driven by can"""
|
||||
|
||||
@@ -217,7 +211,7 @@ class Car:
|
||||
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:
|
||||
# Use CarState w/ buttons from the step selfdrived enables on
|
||||
self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.experimental_mode, self.dynamic_experimental_control)
|
||||
self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.initial_cruise_speed)
|
||||
|
||||
# TODO: mirror the carState.cruiseState struct?
|
||||
CS.vCruise = float(self.v_cruise_helper.v_cruise_kph)
|
||||
@@ -303,9 +297,9 @@ class Car:
|
||||
while not evt.is_set():
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
||||
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True)
|
||||
|
||||
# sunnypilot
|
||||
self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl")
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -13,7 +13,6 @@ V_CRUISE_MIN = 8
|
||||
V_CRUISE_MAX = 145
|
||||
V_CRUISE_UNSET = 255
|
||||
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
|
||||
|
||||
ButtonEvent = car.CarState.ButtonEvent
|
||||
@@ -138,17 +137,14 @@ class VCruiseHelper(VCruiseHelperSP):
|
||||
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}
|
||||
|
||||
def initialize_v_cruise(self, CS, experimental_mode: bool, dynamic_experimental_control: bool) -> None:
|
||||
def initialize_v_cruise(self, CS, initial_speed: int) -> None:
|
||||
# initializing is handled by the PCM
|
||||
if self.CP.pcmCruise:
|
||||
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:
|
||||
self.v_cruise_kph = self.v_cruise_kph_last
|
||||
else:
|
||||
self.v_cruise_kph = int(round(np.clip(CS.vEgo * CV.MS_TO_KPH, initial, V_CRUISE_MAX)))
|
||||
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_cluster_kph = self.v_cruise_kph
|
||||
|
||||
@@ -14,7 +14,7 @@ ButtonEvent = car.CarState.ButtonEvent
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
def run_cruise_simulation(cruise, e2e, personality, t_end=20.):
|
||||
def run_cruise_simulation(cruise, personality, t_end=20.):
|
||||
man = Maneuver(
|
||||
'',
|
||||
duration=t_end,
|
||||
@@ -24,7 +24,6 @@ def run_cruise_simulation(cruise, e2e, personality, t_end=20.):
|
||||
cruise_values=[cruise],
|
||||
prob_lead_values=[0.0],
|
||||
breakpoints=[0.],
|
||||
e2e=e2e,
|
||||
personality=personality,
|
||||
)
|
||||
valid, output = man.evaluate()
|
||||
@@ -32,8 +31,7 @@ def run_cruise_simulation(cruise, e2e, personality, t_end=20.):
|
||||
return output[-1, 3]
|
||||
|
||||
|
||||
@parameterized_class(("e2e", "personality", "speed"), itertools.product(
|
||||
[True, False], # e2e
|
||||
@parameterized_class(("personality", "speed"), itertools.product(
|
||||
log.LongitudinalPersonality.schema.enumerants, # personality
|
||||
[5,35])) # speed
|
||||
class TestCruiseSpeed(OpenpilotTestCase):
|
||||
@@ -41,7 +39,7 @@ class TestCruiseSpeed(OpenpilotTestCase):
|
||||
print(f'Testing {self.speed} m/s')
|
||||
cruise_speed = float(self.speed)
|
||||
|
||||
simulation_steady_state = run_cruise_simulation(cruise_speed, self.e2e, self.personality)
|
||||
simulation_steady_state = run_cruise_simulation(cruise_speed, self.personality)
|
||||
self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {self.speed} m/s')
|
||||
|
||||
|
||||
@@ -59,16 +57,16 @@ class TestVCruiseHelper(OpenpilotTestCase):
|
||||
for _ in range(2):
|
||||
self.v_cruise_helper.update_v_cruise(car.CarState(cruiseState={"available": False}), enabled=False, is_metric=False)
|
||||
|
||||
def enable(self, v_ego, experimental_mode, dynamic_experimental_control):
|
||||
def enable(self, v_ego):
|
||||
# Simulates user pressing set with a current speed
|
||||
self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego), experimental_mode, dynamic_experimental_control)
|
||||
self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego), V_CRUISE_INITIAL)
|
||||
|
||||
def test_adjust_speed(self):
|
||||
"""
|
||||
Asserts speed changes on falling edges of buttons.
|
||||
"""
|
||||
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS)
|
||||
|
||||
for btn in (ButtonType.accelCruise, ButtonType.decelCruise):
|
||||
for pressed in (True, False):
|
||||
@@ -92,7 +90,7 @@ class TestVCruiseHelper(OpenpilotTestCase):
|
||||
CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=pressed)]
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=enabled, is_metric=False)
|
||||
if pressed:
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS)
|
||||
|
||||
# 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)
|
||||
@@ -102,7 +100,7 @@ class TestVCruiseHelper(OpenpilotTestCase):
|
||||
Asserts we don't increment set speed if user presses resume/accel to exit cruise standstill.
|
||||
"""
|
||||
|
||||
self.enable(0, False, False)
|
||||
self.enable(0)
|
||||
|
||||
for standstill in (True, False):
|
||||
for pressed in (True, False):
|
||||
@@ -122,7 +120,7 @@ class TestVCruiseHelper(OpenpilotTestCase):
|
||||
|
||||
for v_ego in np.linspace(0, 100, 101):
|
||||
self.reset_cruise_speed_state()
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS)
|
||||
|
||||
# first decrement speed, then perform gas pressed logic
|
||||
expected_v_cruise_kph = self.v_cruise_helper.v_cruise_kph - IMPERIAL_INCREMENT
|
||||
@@ -143,12 +141,10 @@ class TestVCruiseHelper(OpenpilotTestCase):
|
||||
Asserts allowed cruise speeds on enabling with SET.
|
||||
"""
|
||||
|
||||
for experimental_mode in (True, False):
|
||||
for dynamic_experimental_control in (True, False):
|
||||
for v_ego in np.linspace(0, 100, 101):
|
||||
self.reset_cruise_speed_state()
|
||||
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), experimental_mode, dynamic_experimental_control)
|
||||
assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX
|
||||
assert self.v_cruise_helper.v_cruise_initialized
|
||||
self.enable(float(v_ego))
|
||||
assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX
|
||||
assert self.v_cruise_helper.v_cruise_initialized
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import numpy as np
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
ACCEL_BOOST_MAX = 0.2
|
||||
ACCEL_BOOST_RATE = 0.025
|
||||
ACCEL_BOOST_PER_OVERRIDE = 0.05
|
||||
ACCEL_BOOST_MIN_SPEED = 10 * CV.MPH_TO_MS
|
||||
|
||||
|
||||
class AccelBoost:
|
||||
def __init__(self):
|
||||
self.total_boost = 0.0
|
||||
self.boost_this_override = 0.0
|
||||
self.boost_eligible = False
|
||||
|
||||
def update(self, sm, output_a_target_e2e, output_a_target_mpc, a_cruise):
|
||||
enabled = sm['selfdriveState'].enabled
|
||||
gas_pressed = sm['carState'].gasPressed
|
||||
v_ego = sm['carState'].vEgo
|
||||
speed_scale = np.interp(v_ego, [ACCEL_BOOST_MIN_SPEED, 2 * ACCEL_BOOST_MIN_SPEED], [0.0, 1.0])
|
||||
boost_scale = speed_scale * np.interp(output_a_target_e2e, [-1.0, -0.5, 5.0], [0.0, 1.0, 1.0], right=0.0)
|
||||
model_limited = output_a_target_e2e + self.total_boost * boost_scale < min(output_a_target_mpc, a_cruise) - 0.1
|
||||
|
||||
if not enabled or not gas_pressed:
|
||||
self.boost_this_override = 0.0
|
||||
# Latch eligibility before the override changes the lead plan.
|
||||
self.boost_eligible = enabled and model_limited
|
||||
|
||||
if not enabled:
|
||||
self.total_boost = 0.0
|
||||
elif gas_pressed and self.boost_eligible:
|
||||
increase = min(ACCEL_BOOST_RATE * DT_MDL * speed_scale,
|
||||
ACCEL_BOOST_PER_OVERRIDE - self.boost_this_override, ACCEL_BOOST_MAX - self.total_boost)
|
||||
self.total_boost += increase
|
||||
self.boost_this_override += increase
|
||||
|
||||
return output_a_target_e2e + self.total_boost * boost_scale
|
||||
@@ -4,7 +4,6 @@ from openpilot.common.realtime import DT_CTRL, DT_MDL
|
||||
|
||||
MIN_SPEED = 1.0
|
||||
CONTROL_N = 17
|
||||
CAR_ROTATION_RADIUS = 0.0
|
||||
# This is a turn radius smaller than most cars can achieve
|
||||
MAX_CURVATURE = 0.2
|
||||
MIN_STABLE_DELAY = 0.3
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import math
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.pid import PIDController
|
||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import MAX_CURVATURE
|
||||
|
||||
CURVATURE_SATURATION_THRESHOLD = 1e-3 # 1/m
|
||||
LAT_ACCEL_SATURATION_THRESHOLD = 0.5 # m/s^2
|
||||
|
||||
|
||||
class LatControlCurvature(LatControl):
|
||||
@@ -20,6 +22,8 @@ class LatControlCurvature(LatControl):
|
||||
else:
|
||||
self.pid = None
|
||||
self.kf = 1.
|
||||
self.desired_curvature_buffer_len = int(1. / self.dt)
|
||||
self.desired_curvature_buffer = deque([0.] * self.desired_curvature_buffer_len, maxlen=self.desired_curvature_buffer_len)
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
@@ -30,6 +34,9 @@ class LatControlCurvature(LatControl):
|
||||
curvature_log = log.ControlsState.LateralCurvatureState.new_message()
|
||||
actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
||||
error = desired_curvature - actual_curvature
|
||||
self.desired_curvature_buffer.append(desired_curvature)
|
||||
delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.desired_curvature_buffer_len))
|
||||
expected_curvature = self.desired_curvature_buffer[-delay_frames]
|
||||
|
||||
if not active:
|
||||
output_curvature = 0.0
|
||||
@@ -53,6 +60,6 @@ class LatControlCurvature(LatControl):
|
||||
curvature_log.actualCurvature = float(actual_curvature)
|
||||
curvature_log.desiredCurvature = float(desired_curvature)
|
||||
curvature_log.output = float(output_curvature)
|
||||
curvature_log.saturated = bool(self._check_saturation(abs(error) > CURVATURE_SATURATION_THRESHOLD, CS,
|
||||
curvature_log.saturated = bool(self._check_saturation(abs(expected_curvature - actual_curvature) * CS.vEgo ** 2 > LAT_ACCEL_SATURATION_THRESHOLD, CS,
|
||||
False, curvature_limited))
|
||||
return 0.0, float(output_curvature), curvature_log
|
||||
|
||||
@@ -8,6 +8,7 @@ from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||
@@ -25,7 +26,6 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
||||
ALLOW_THROTTLE_THRESHOLD = 0.4
|
||||
MIN_ALLOW_THROTTLE_SPEED = 2.5
|
||||
|
||||
# Lookup table for turns
|
||||
_A_TOTAL_MAX_V = [1.7, 3.2]
|
||||
_A_TOTAL_MAX_BP = [20., 40.]
|
||||
|
||||
@@ -33,7 +33,7 @@ def get_max_accel(v_ego):
|
||||
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
|
||||
|
||||
def get_coast_accel(pitch):
|
||||
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
|
||||
return np.sin(pitch) * -5.65 - 0.3
|
||||
|
||||
def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle):
|
||||
max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego)
|
||||
@@ -62,6 +62,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
LongitudinalPlannerSP.__init__(self, self.CP, CP_SP, self.mpc)
|
||||
self.fcw = False
|
||||
self.dt = dt
|
||||
self.accel_boost = AccelBoost()
|
||||
self.allow_throttle = True
|
||||
|
||||
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
|
||||
@@ -89,18 +90,18 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
|
||||
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
|
||||
|
||||
# Reset current state when not engaged, or user is controlling the speed
|
||||
reset_state = long_control_off if self.CP.openpilotLongitudinalControl else not sm['selfdriveState'].enabled
|
||||
# PCM cruise speed may be updated a few cycles later, check if initialized
|
||||
v_cruise_initialized = sm['carState'].vCruise != V_CRUISE_UNSET
|
||||
reset_state = reset_state or not v_cruise_initialized
|
||||
|
||||
throttle_probs = sm['modelV2'].meta.disengagePredictions.gasPressProbs
|
||||
throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0
|
||||
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
|
||||
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['vehicleParameters'].angleOffsetDeg
|
||||
|
||||
# Reset current state when not engaged, or user is controlling the speed
|
||||
reset_state = long_control_off if self.CP.openpilotLongitudinalControl else not sm['selfdriveState'].enabled
|
||||
# PCM cruise speed may be updated a few cycles later, check if initialized
|
||||
v_cruise_initialized = sm['carState'].vCruise != V_CRUISE_UNSET
|
||||
reset_state = reset_state or not v_cruise_initialized
|
||||
|
||||
if reset_state:
|
||||
self.v_desired_filter.x = v_ego
|
||||
self.output_a_target = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX)
|
||||
@@ -116,7 +117,10 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
v_cruise, self.output_a_target = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.output_a_target, v_cruise)
|
||||
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target)
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target)
|
||||
else:
|
||||
self.mpc.set_cur_state(v_ego, sm['carState'].aEgo)
|
||||
self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
@@ -138,16 +142,18 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||
|
||||
is_e2e = self.is_e2e(sm)
|
||||
experimental_mode = sm['selfdriveState'].deprecated.experimentalMode
|
||||
|
||||
self.a_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego,
|
||||
self.a_cruise = get_cruise_accel(experimental_mode, v_cruise, v_ego,
|
||||
self.a_cruise, steer_angle_without_offset, self.CP, self.dt,
|
||||
accel_coast, self.allow_throttle)
|
||||
cruise_should_stop = should_stop(v_ego, self.a_cruise)
|
||||
|
||||
output_a_target_e2e = self.accel_boost.update(sm, output_a_target_e2e, output_a_target_mpc, self.a_cruise)
|
||||
|
||||
candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc),
|
||||
(self.a_cruise, LongitudinalPlanSource.cruise, cruise_should_stop)]
|
||||
if is_e2e:
|
||||
if experimental_mode:
|
||||
candidates.append((output_a_target_e2e, LongitudinalPlanSource.e2e, output_should_stop_e2e))
|
||||
|
||||
output_a_target, self.mpc.source, _ = min(candidates, key=lambda c: c[0])
|
||||
@@ -175,6 +181,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
longitudinalPlan.fcw = self.fcw
|
||||
|
||||
longitudinalPlan.aTarget = float(self.output_a_target)
|
||||
longitudinalPlan.accelBoost = float(self.accel_boost.total_boost)
|
||||
longitudinalPlan.shouldStop = bool(self.output_should_stop)
|
||||
longitudinalPlan.allowBrake = True
|
||||
longitudinalPlan.allowThrottle = bool(self.allow_throttle)
|
||||
|
||||
@@ -13,7 +13,7 @@ def desired_follow_distance(v_ego, v_lead, t_follow=None):
|
||||
t_follow = get_T_FOLLOW()
|
||||
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
|
||||
|
||||
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
|
||||
def run_following_distance_simulation(v_lead, t_end=100.0, personality=0):
|
||||
man = Maneuver(
|
||||
'',
|
||||
duration=t_end,
|
||||
@@ -22,7 +22,6 @@ def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personalit
|
||||
initial_distance_lead=100,
|
||||
speed_lead_values=[v_lead],
|
||||
breakpoints=[0.],
|
||||
e2e=e2e,
|
||||
personality=personality,
|
||||
)
|
||||
valid, output = man.evaluate()
|
||||
@@ -30,8 +29,7 @@ def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personalit
|
||||
return output[-1,2] - output[-1,1]
|
||||
|
||||
|
||||
@parameterized_class(("e2e", "personality", "speed"), itertools.product(
|
||||
[True, False], # e2e
|
||||
@parameterized_class(("personality", "speed"), itertools.product(
|
||||
[log.LongitudinalPersonality.relaxed, # personality
|
||||
log.LongitudinalPersonality.standard,
|
||||
log.LongitudinalPersonality.aggressive],
|
||||
@@ -39,9 +37,8 @@ def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personalit
|
||||
class TestFollowingDistance(OpenpilotTestCase):
|
||||
def test_following_distance(self):
|
||||
v_lead = float(self.speed)
|
||||
simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality)
|
||||
simulation_steady_state = run_following_distance_simulation(v_lead, personality=self.personality)
|
||||
correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality))
|
||||
err_ratio = 0.2 if self.e2e else 0.1
|
||||
abs_err_margin = 0.5 if v_lead > 0.0 else 1.15
|
||||
self.assertAlmostEqual(simulation_steady_state, correct_steady_state,
|
||||
delta=err_ratio * correct_steady_state + abs_err_margin)
|
||||
delta=0.2 * correct_steady_state + abs_err_margin)
|
||||
|
||||
@@ -8,12 +8,14 @@ from opendbc.car.honda.values import CAR as HONDA
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from opendbc.car.nissan.values import CAR as NISSAN
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature
|
||||
from openpilot.selfdrive.locationd.helpers import Pose
|
||||
from openpilot.common.mock.generators import generate_deviceMotion
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
@@ -22,7 +24,8 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac
|
||||
class TestLatControl(OpenpilotTestCase):
|
||||
|
||||
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque),
|
||||
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)])
|
||||
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque),
|
||||
(VOLKSWAGEN.VOLKSWAGEN_ID4_MK2, LatControlCurvature)])
|
||||
def test_saturation(self, car_name, controller):
|
||||
CarInterface = interfaces[car_name]
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
|
||||
@@ -278,12 +278,12 @@ def main():
|
||||
estimator = LocationEstimator(DEBUG)
|
||||
|
||||
filter_initialized = False
|
||||
critcal_services = ["accelerometer", "gyroscope", "cameraOdometry"]
|
||||
critical_services = ["accelerometer", "gyroscope", "cameraOdometry"]
|
||||
observation_input_invalid = defaultdict(int)
|
||||
|
||||
input_invalid_limit = {s: round(INPUT_INVALID_LIMIT * (SERVICE_LIST[s].frequency / 20.)) for s in critcal_services}
|
||||
input_invalid_threshold = {s: input_invalid_limit[s] - 0.5 for s in critcal_services}
|
||||
input_invalid_decay = {s: calculate_invalid_input_decay(input_invalid_limit[s], INPUT_INVALID_RECOVERY, SERVICE_LIST[s].frequency) for s in critcal_services}
|
||||
input_invalid_limit = {s: round(INPUT_INVALID_LIMIT * (SERVICE_LIST[s].frequency / 20.)) for s in critical_services}
|
||||
input_invalid_threshold = {s: input_invalid_limit[s] - 0.5 for s in critical_services}
|
||||
input_invalid_decay = {s: calculate_invalid_input_decay(input_invalid_limit[s], INPUT_INVALID_RECOVERY, SERVICE_LIST[s].frequency) for s in critical_services}
|
||||
|
||||
initial_pose_data = params.get("LocationFilterInitialState")
|
||||
if initial_pose_data is not None:
|
||||
@@ -313,7 +313,7 @@ def main():
|
||||
if valid:
|
||||
t = log_mono_time * 1e-9
|
||||
res = estimator.handle_log(t, which, msg)
|
||||
if which not in critcal_services:
|
||||
if which not in critical_services:
|
||||
continue
|
||||
|
||||
if res == HandleLogResult.TIMING_INVALID:
|
||||
@@ -328,7 +328,7 @@ def main():
|
||||
filter_initialized = sm.all_checks() and sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
|
||||
|
||||
if sm.updated["cameraOdometry"]:
|
||||
critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critcal_services)
|
||||
critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critical_services)
|
||||
inputs_valid = sm.all_valid() and critical_service_inputs_valid
|
||||
sensors_valid = sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ class CarKalman(KalmanFilter):
|
||||
}
|
||||
|
||||
global_vars = [
|
||||
'mass',
|
||||
'rotational_inertia',
|
||||
'center_to_front',
|
||||
'center_to_rear',
|
||||
@@ -99,7 +98,10 @@ class CarKalman(KalmanFilter):
|
||||
|
||||
# globals
|
||||
global_vars = [sp.Symbol(name) for name in CarKalman.global_vars]
|
||||
m, j, aF, aR, cF_orig, cR_orig = global_vars
|
||||
j, aF, aR, cF_orig, cR_orig = global_vars
|
||||
# Tire stiffness and rotational inertia are already divided by vehicle mass, which cancels out of the model.
|
||||
# Use unit mass to preserve the standard bicycle-model equations below.
|
||||
m = 1
|
||||
|
||||
# make functions and jacobians with sympy
|
||||
# state variables
|
||||
@@ -166,8 +168,7 @@ class CarKalman(KalmanFilter):
|
||||
self.filter = EKF_sym_pyx(generated_dir, CarKalman.name, CarKalman.Q, CarKalman.initial_x, CarKalman.P_initial,
|
||||
dim_state, dim_state_err, global_vars=CarKalman.global_vars, logger=cloudlog)
|
||||
|
||||
def set_globals(self, mass, rotational_inertia, center_to_front, center_to_rear, stiffness_front, stiffness_rear):
|
||||
self.filter.set_global("mass", mass)
|
||||
def set_globals(self, rotational_inertia, center_to_front, center_to_rear, stiffness_front, stiffness_rear):
|
||||
self.filter.set_global("rotational_inertia", rotational_inertia)
|
||||
self.filter.set_global("center_to_front", center_to_front)
|
||||
self.filter.set_global("center_to_rear", center_to_rear)
|
||||
|
||||
@@ -40,7 +40,7 @@ class PoseKalman(KalmanFilter):
|
||||
initial_P = np.diag([0.01**2, 0.01**2, 0.01**2,
|
||||
10**2, 10**2, 10**2,
|
||||
1**2, 1**2, 1**2,
|
||||
1**2, 1**2, 1**2,
|
||||
0.1**2, 0.1**2, 0.1**2,
|
||||
100**2, 100**2, 100**2,
|
||||
0.01**2, 0.01**2, 0.01**2])
|
||||
|
||||
|
||||
@@ -36,12 +36,11 @@ class VehicleParamsLearner:
|
||||
self.P_initial = P_initial if P_initial is not None else CarKalman.P_initial
|
||||
|
||||
self.kf.set_globals(
|
||||
mass=CP.mass,
|
||||
rotational_inertia=CP.rotationalInertia,
|
||||
rotational_inertia=CP.rotationalInertiaPerMass,
|
||||
center_to_front=CP.centerToFront,
|
||||
center_to_rear=CP.wheelbase - CP.centerToFront,
|
||||
stiffness_front=CP.tireStiffnessFront,
|
||||
stiffness_rear=CP.tireStiffnessRear
|
||||
stiffness_front=CP.tireStiffnessFrontPerMass,
|
||||
stiffness_rear=CP.tireStiffnessRearPerMass
|
||||
)
|
||||
|
||||
self.min_sr, self.max_sr = 0.5 * CP.steerRatio, 2.0 * CP.steerRatio
|
||||
|
||||
@@ -1,142 +1,71 @@
|
||||
import ctypes.util
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from SCons.Script import Action, Value
|
||||
from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_existing_chunks
|
||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
|
||||
|
||||
Import('env', 'arch')
|
||||
chunker_file = File("#openpilot/common/file_chunker.py")
|
||||
|
||||
# TODO: add GCC support to tinygrad?
|
||||
# tinygrad's CPU:LLVM backend requires libLLVM, skip the build on PC if it's not available
|
||||
if arch not in ('comma_arm64', 'Darwin') and ctypes.util.find_library('LLVM') is None:
|
||||
print("\033[93mmodeld: LLVM not found, skipping tinygrad model compilation. Install LLVM to compile models.\033[0m")
|
||||
Return()
|
||||
|
||||
lenv = env.Clone()
|
||||
lenv.PrependENVPath('PYTHONPATH', Dir('#tinygrad_repo').abspath)
|
||||
|
||||
tinygrad_root = env.Dir("#").abspath
|
||||
tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root)
|
||||
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
|
||||
|
||||
def estimate_pickle_max_size(onnx_size):
|
||||
# QCOM programs for models with spatial recurrent features can approach 2x
|
||||
# the ONNX size. Overestimating only adds an empty trailing chunk.
|
||||
return 2.0 * onnx_size + 10 * 1024 * 1024
|
||||
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
|
||||
|
||||
if arch == 'comma_arm64':
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
|
||||
camera_configs = [(camera.width, camera.height)]
|
||||
tg_backend = 'QCOM'
|
||||
tg_flags = f'DEV={tg_backend} 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'
|
||||
else:
|
||||
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
|
||||
tg_backend = 'CPU'
|
||||
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
||||
|
||||
tg_devices = { # which device to put jit inputs to at runtime
|
||||
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
||||
'default': {'DEV': tg_backend}
|
||||
},
|
||||
}
|
||||
|
||||
CHESTNUT = chestnut_present()
|
||||
if CHESTNUT:
|
||||
chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
|
||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||
chestnut_lock = File("models/.chestnut.lock").abspath
|
||||
|
||||
def write_tg_devices(target, source, env):
|
||||
with open(str(target[0]), "w") as f:
|
||||
json.dump(tg_devices, f)
|
||||
f.write("\n")
|
||||
|
||||
tg_devices_node = lenv.Command(
|
||||
str(TG_INPUT_DEVICES_PATH),
|
||||
[Value(tg_devices)],
|
||||
write_tg_devices,
|
||||
)
|
||||
# JIT=2 disables graph batching, which produces incorrect outputs after buffers change.
|
||||
tg_flags = 'DEV=METAL JIT=2' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
||||
|
||||
# tinygrad calls brew which needs a $HOME in the env
|
||||
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
|
||||
|
||||
modeld_dir = Dir("#openpilot/selfdrive/modeld").abspath
|
||||
compile_modeld_script = [
|
||||
File(f"{modeld_dir}/compile_modeld.py"),
|
||||
File(f"{modeld_dir}/get_model_metadata.py"),
|
||||
File("#openpilot/system/camerad/cameras/nv12_info.py"),
|
||||
File("#openpilot/common/hardware/hw.py"),
|
||||
]
|
||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
warp_deps = [File("#openpilot/system/camerad/cameras/nv12_info.py")]
|
||||
compiler = Dir('#tinygrad_repo/examples/openpilot').abspath
|
||||
# build on all cores
|
||||
taskset = 'taskset -c 0-7 ' if arch == 'comma_arm64' else ''
|
||||
|
||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
for chestnut in [False, True] if CHESTNUT else [False]:
|
||||
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
|
||||
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
|
||||
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
||||
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
||||
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
|
||||
f'--model-size {model_w}x{model_h} '
|
||||
f'--camera-resolutions {camera_res_args} '
|
||||
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
|
||||
f'--output {target_pkl_path} --frame-skip {frame_skip}')
|
||||
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
|
||||
chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
|
||||
def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets):
|
||||
from openpilot.system.hardware.chestnut.flash import link_up
|
||||
# chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars
|
||||
for _ in range(10):
|
||||
if link_up():
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
print("Chestnut not ready, skipping big model build")
|
||||
return
|
||||
if ret := env.Execute(command):
|
||||
return ret
|
||||
chunk_file(pkl, chunks)
|
||||
def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets):
|
||||
chunk_file(pkl, chunks)
|
||||
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
|
||||
node = lenv.Command(
|
||||
chunk_targets,
|
||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
|
||||
actions,
|
||||
)
|
||||
if chestnut:
|
||||
lenv.SideEffect(chestnut_lock, node)
|
||||
|
||||
# get model metadata
|
||||
fn = File(f"models/dmonitoring_model").abspath
|
||||
script_files = [File(Dir("#openpilot/selfdrive/modeld").File("get_model_metadata.py").abspath)]
|
||||
cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#openpilot/selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
|
||||
lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files + [tg_devices_node], cmd)
|
||||
|
||||
dm_w, dm_h = DM_INPUT_SIZE
|
||||
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
||||
for cam_w, cam_h in camera_configs:
|
||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
|
||||
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
|
||||
f'--output {dm_pkl_path}')
|
||||
lenv.Command(dm_pkl_path, tinygrad_files + compile_dm_warp_script + compile_modeld_script + [tg_devices_node], cmd)
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
pkl = fn + "_tinygrad.pkl"
|
||||
onnx_path = fn + ".onnx"
|
||||
chunk_targets = get_chunk_targets(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path)))
|
||||
def do_chunk(target, source, env):
|
||||
chunk_file(pkl, chunk_targets)
|
||||
return lenv.Command(
|
||||
chunk_targets,
|
||||
[onnx_path] + tinygrad_files + [Value(chunk_targets), chunker_file, tg_devices_node],
|
||||
[f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}',
|
||||
Action(do_chunk, " [CHUNK] $TARGET")],
|
||||
def compile_model(onnx_path, pkl_path):
|
||||
onnx_path, target_pkl_path = File(onnx_path).abspath, File(pkl_path).abspath
|
||||
cmd = (f'{tg_flags} {mac_brew_string} {taskset}python3 "{compiler}/compile_onnx.py" '
|
||||
f'"{onnx_path}" "{target_pkl_path}" --out-of-band --benchmark-runs 1')
|
||||
lenv.Command(
|
||||
target_pkl_path,
|
||||
tinygrad_files + [onnx_path, Value(cmd)],
|
||||
Action(cmd, " [ONNX] $TARGET"),
|
||||
)
|
||||
|
||||
tg_compile(tg_flags, 'dmonitoring_model')
|
||||
compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygrad.pkl')
|
||||
compile_model('models/driving_supercombo.onnx', 'models/driving_tinygrad.pkl')
|
||||
|
||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
for cam_w, cam_h in camera_configs:
|
||||
warp_pkl_path = File(f"models/driving_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||
cmd = (f'{tg_flags} {mac_brew_string} {taskset}python3 "{compiler}/compile_warp.py" '
|
||||
f'--frame {cam_w},{cam_h},{stride},{y_height},{uv_height},{stride * (y_height + uv_height)} '
|
||||
f'--warp-to {model_w}x{model_h} --layout yuv420 --frames 2 '
|
||||
f'--output {warp_pkl_path}')
|
||||
lenv.Command(warp_pkl_path, tinygrad_files + warp_deps + [Value(cmd)], cmd)
|
||||
|
||||
dm_w, dm_h = DM_INPUT_SIZE
|
||||
for cam_w, cam_h in camera_configs:
|
||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
stride, y_height, uv_height, frame_size = get_nv12_info(cam_w, cam_h)
|
||||
cmd = (f'{tg_flags} {mac_brew_string} python3 "{compiler}/compile_warp.py" '
|
||||
f'--frame {cam_w},{cam_h},{stride},{y_height},{uv_height},{frame_size} --warp-to {dm_w}x{dm_h} '
|
||||
f'--layout luma --border-fill 16 --transform-device NPY --output {dm_pkl_path}')
|
||||
lenv.Command(dm_pkl_path, tinygrad_files + warp_deps + [Value(cmd)], cmd)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import pickle
|
||||
import time
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, warp_perspective_tinygrad, _parse_size
|
||||
|
||||
|
||||
def make_warp_dm(nv12: NV12Frame, dm_w, dm_h):
|
||||
cam_w, cam_h, stride, _, _, _ = nv12
|
||||
stride_pad = stride - cam_w
|
||||
|
||||
def warp_dm(input_frame, M_inv):
|
||||
M_inv = M_inv.to(Device.DEFAULT).realize()
|
||||
return warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv,
|
||||
(dm_w, dm_h), (cam_h, cam_w), stride_pad, border_fill_val=16).reshape(-1, dm_h * dm_w) # Y
|
||||
return warp_dm
|
||||
|
||||
|
||||
def compile_dm_warp(nv12: NV12Frame, dm_w, dm_h, pkl_path):
|
||||
print(f"Compiling DM warp for {nv12.width}x{nv12.height} -> {dm_w}x{dm_h}...")
|
||||
|
||||
warp_dm_jit = TinyJit(make_warp_dm(nv12, dm_w, dm_h), prune=True)
|
||||
|
||||
for i in range(10):
|
||||
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
||||
M_inv = Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')
|
||||
Device.default.synchronize()
|
||||
st = time.perf_counter()
|
||||
warp_dm_jit(frame, M_inv).realize()
|
||||
mt = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
et = time.perf_counter()
|
||||
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||
|
||||
with open(pkl_path, "wb") as f:
|
||||
pickle.dump(warp_dm_jit, f)
|
||||
print(f" Saved to {pkl_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--camera-resolution', type=_parse_size, required=True, help='camera resolution WxH')
|
||||
p.add_argument('--warp-to', type=_parse_size, required=True, help='DM input WxH')
|
||||
p.add_argument('--output', required=True)
|
||||
args = p.parse_args()
|
||||
|
||||
cam_w, cam_h = args.camera_resolution
|
||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
dm_w, dm_h = args.warp_to
|
||||
compile_dm_warp(nv12, dm_w, dm_h, args.output)
|
||||
@@ -1,332 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import atexit
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import shutil
|
||||
from functools import partial
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob
|
||||
|
||||
def _patch_tinygrad_fetch_fw():
|
||||
import hashlib
|
||||
import pathlib
|
||||
import zstandard
|
||||
from tinygrad import helpers
|
||||
_orig = helpers.fetch_fw
|
||||
def fetch_fw(path, name, sha256):
|
||||
p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
|
||||
if p.is_file():
|
||||
blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read()
|
||||
if hashlib.sha256(blob).hexdigest() == sha256:
|
||||
return blob
|
||||
return _orig(path, name, sha256)
|
||||
helpers.fetch_fw = fetch_fw
|
||||
_patch_tinygrad_fetch_fw()
|
||||
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
|
||||
|
||||
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
||||
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||
|
||||
|
||||
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
|
||||
# Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
|
||||
return stride * (y_height + uv_height)
|
||||
|
||||
|
||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
w_dst, h_dst = dst_shape
|
||||
h_src, w_src = src_shape
|
||||
|
||||
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
||||
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
||||
|
||||
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
||||
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
||||
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
||||
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
||||
|
||||
src_x = src_x / src_w
|
||||
src_y = src_y / src_w
|
||||
|
||||
x_round = Tensor.round(src_x)
|
||||
y_round = Tensor.round(src_y)
|
||||
x_nn_clipped = x_round.clip(0, w_src - 1).cast('int')
|
||||
y_nn_clipped = y_round.clip(0, h_src - 1).cast('int')
|
||||
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
||||
sampled = src_flat[idx]
|
||||
|
||||
if border_fill_val is None:
|
||||
return sampled
|
||||
|
||||
in_bounds = ((x_round >= 0) & (x_round <= w_src - 1) &
|
||||
(y_round >= 0) & (y_round <= h_src - 1)).cast(sampled.dtype)
|
||||
return sampled * in_bounds + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - in_bounds)
|
||||
|
||||
|
||||
def frames_to_tensor(frames):
|
||||
H = (frames.shape[0] * 2) // 3
|
||||
W = frames.shape[1]
|
||||
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
||||
frames[1:H:2, 0::2],
|
||||
frames[0:H:2, 1::2],
|
||||
frames[1:H:2, 1::2],
|
||||
frames[H:H+H//4].reshape((H//2, W//2)),
|
||||
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
|
||||
return in_img1
|
||||
|
||||
|
||||
def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
||||
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
|
||||
uv_offset = stride * y_height
|
||||
stride_pad = stride - cam_w
|
||||
|
||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
|
||||
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||
with Context(SPLIT_REDUCEOP=0):
|
||||
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
|
||||
M_inv, (model_w, model_h),
|
||||
(cam_h, cam_w), stride_pad).realize()
|
||||
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
|
||||
M_inv_uv, (model_w//2, model_h//2),
|
||||
(cam_h//2, cam_w//2), 0).realize()
|
||||
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
|
||||
M_inv_uv, (model_w//2, model_h//2),
|
||||
(cam_h//2, cam_w//2), 0).realize()
|
||||
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
|
||||
tensor = frames_to_tensor(yuv)
|
||||
return tensor
|
||||
return frame_prepare_tinygrad
|
||||
|
||||
|
||||
def get_policy_npy_shapes(input_shapes):
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
tc = input_shapes['traffic_convention'] # (1, 2)
|
||||
at = input_shapes['action_t'] # (1, 2)
|
||||
fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
|
||||
feat_dim = math.prod(fb[2:])
|
||||
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
||||
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
|
||||
return shapes, [math.prod(s) for s in shapes.values()]
|
||||
|
||||
|
||||
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
|
||||
img = input_shapes['img'] # (1, 12, 128, 256)
|
||||
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
|
||||
feat_dim = math.prod(fb[2:])
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
n_frames = img[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
|
||||
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
|
||||
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
|
||||
sizes = [math.prod(s) for s in shapes.values()]
|
||||
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
|
||||
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
|
||||
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
|
||||
frames = packed_input[packed_npy_size:]
|
||||
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
|
||||
# views into the packed inputs, to be refilled at runtime
|
||||
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
|
||||
input_queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
|
||||
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
|
||||
}
|
||||
return input_queues, npy, frame_views
|
||||
|
||||
|
||||
def shift_and_sample(buf, new_val, sample_fn):
|
||||
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
|
||||
return sample_fn(buf)
|
||||
|
||||
|
||||
def sample_skip(buf, frame_skip):
|
||||
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def sample_desire(buf, frame_skip):
|
||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def make_warp(nv12, model_w, model_h):
|
||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||
|
||||
def warp(tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(Device.DEFAULT)
|
||||
big_tfm = big_tfm.to(Device.DEFAULT)
|
||||
frame = frame.to(Device.DEFAULT)
|
||||
big_frame = big_frame.to(Device.DEFAULT)
|
||||
Tensor.realize(tfm, big_tfm, frame, big_frame)
|
||||
|
||||
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
||||
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
||||
return Tensor.cat(warped_frame, warped_big_frame)
|
||||
|
||||
return warp
|
||||
|
||||
|
||||
def make_run_policy(model_runner, model_metadata, frame_skip):
|
||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
|
||||
|
||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_npy_inputs, warped)
|
||||
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||
big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn)
|
||||
|
||||
desire, traffic_convention, action_t, prev_feat = (t.reshape(s) for t, s in zip(packed_npy_inputs.split(npy_sizes), npy_shapes.values(), strict=True))
|
||||
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
||||
feat_buf = shift_and_sample(feat_q, prev_feat.reshape(1, 1, -1), sample_skip_fn)
|
||||
|
||||
inputs = {
|
||||
'img': img,
|
||||
'big_img': big_img,
|
||||
'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
|
||||
'desire_pulse': desire_buf,
|
||||
'traffic_convention': traffic_convention,
|
||||
'action_t': action_t,
|
||||
}
|
||||
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
|
||||
out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||
return out,
|
||||
return run_policy
|
||||
|
||||
|
||||
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
|
||||
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
|
||||
|
||||
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_input = packed_npy_inputs.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_input)
|
||||
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
|
||||
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
|
||||
big_frame = packed_input[packed_npy_size + frame_copy_size:]
|
||||
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
|
||||
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
|
||||
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
|
||||
return run_model
|
||||
|
||||
|
||||
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
|
||||
if benchmark_runs < 1:
|
||||
raise ValueError("benchmark_runs must be at least 1")
|
||||
|
||||
SEED = 42
|
||||
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
for i in range(n_runs):
|
||||
for v in npy.values():
|
||||
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||
for v in frame_views.values():
|
||||
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
|
||||
Device.default.synchronize()
|
||||
st = time.perf_counter()
|
||||
outs = fn(**{k: input_queues[k] for k in input_keys})
|
||||
mt = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
et = time.perf_counter()
|
||||
print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||
|
||||
if i == 0:
|
||||
val = [np.copy(v.numpy()) for v in outs]
|
||||
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
|
||||
|
||||
if test_val is not None:
|
||||
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True))
|
||||
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
||||
if test_buffers is not None:
|
||||
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
|
||||
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
||||
return val, buffers
|
||||
|
||||
print('capture + replay')
|
||||
test_val, test_buffers = random_inputs_run(jit, SEED, 3)
|
||||
print(f'pickle round trip ({benchmark_runs} runs per seed)')
|
||||
with tempfile.TemporaryFile(dir=".") as f:
|
||||
dump_oob(jit, f)
|
||||
f.seek(0)
|
||||
loaded_jit = load_oob(f)
|
||||
random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
|
||||
random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
|
||||
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
|
||||
return jit
|
||||
|
||||
|
||||
def _parse_size(s):
|
||||
w, h = s.lower().split('x')
|
||||
return int(w), int(h)
|
||||
|
||||
|
||||
def read_file_chunked_to_disk(path):
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
tmp_path = f'{path}.unchunked'
|
||||
with open(tmp_path, 'wb') as f, open_file_chunked(path) as src:
|
||||
shutil.copyfileobj(src, f)
|
||||
atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
|
||||
p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True,
|
||||
help='camera resolutions WxH (one or more)')
|
||||
p.add_argument('--onnx', required=True)
|
||||
p.add_argument('--output', required=True)
|
||||
p.add_argument('--frame-skip', type=int, required=True)
|
||||
p.add_argument('--benchmark-runs', type=int, default=1,
|
||||
help='timed loaded-JIT runs for each correctness seed')
|
||||
args = p.parse_args()
|
||||
|
||||
model_path = read_file_chunked_to_disk(args.onnx)
|
||||
model_w, model_h = args.model_size
|
||||
|
||||
model_runner = OnnxRunner(model_path)
|
||||
out = {
|
||||
'metadata': make_metadata_dict(model_path),
|
||||
'input_devices': {'model': Device.DEFAULT},
|
||||
'run_model': {},
|
||||
}
|
||||
|
||||
run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
|
||||
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
|
||||
make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
|
||||
frame_copy_size=frame_copy_size)
|
||||
warp = make_warp(nv12, model_w, model_h)
|
||||
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
|
||||
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
|
||||
args.benchmark_runs)
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
dump_oob(out, f)
|
||||
print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user