mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-17 08:23:44 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72deb91942 | |||
| d5b5652165 |
+1
-3
@@ -1,10 +1,8 @@
|
||||
* text=auto eol=lf
|
||||
* text=auto
|
||||
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
* @sunnypilot/dev-internal
|
||||
/.github/ @devtekve @sunnyhaibin
|
||||
/release/ci/ @devtekve @sunnyhaibin
|
||||
/tinygrad_repo @devtekve @Discountchubbs
|
||||
/tinygrad/ @devtekve @Discountchubbs
|
||||
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
|
||||
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
|
||||
/selfdrive/modeld/ @devtekve @Discountchubbs
|
||||
/sunnypilot/model* @devtekve @Discountchubbs
|
||||
/sunnypilot/sunnylink/ @devtekve
|
||||
/system/athena/ @devtekve
|
||||
@@ -0,0 +1,43 @@
|
||||
exclude-labels:
|
||||
- 'no-changelog'
|
||||
categories:
|
||||
- title: '🚀 Features'
|
||||
labels:
|
||||
- 'feature'
|
||||
- 'enhancement'
|
||||
- title: '🐛 Bug Fixes'
|
||||
collapse-after: 5
|
||||
labels:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '🧰 Maintenance'
|
||||
collapse-after: 5
|
||||
label: 'chore'
|
||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||
change-title-escapes: '\<*_&'
|
||||
replacers:
|
||||
- search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g'
|
||||
replace: 'sunnypilot'
|
||||
- search: '/\b[Ss][Pp]\b/g'
|
||||
replace: 'SP'
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'major'
|
||||
minor:
|
||||
labels:
|
||||
- 'minor'
|
||||
patch:
|
||||
labels:
|
||||
- 'patch'
|
||||
default: patch
|
||||
name-template: 'v$RESOLVED_VERSION 🚀'
|
||||
tag-template: 'v$RESOLVED_VERSION'
|
||||
version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this
|
||||
tag-prefix: "v0." # The day OP becomes v1, we need to bump this
|
||||
prerelease-identifier: "staging"
|
||||
template: |
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
@@ -20,11 +20,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
docs_repo:
|
||||
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot-models'
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
@@ -39,6 +34,7 @@ jobs:
|
||||
- name: Checkout sunnypilot repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot
|
||||
path: sunnypilot
|
||||
submodules: recursive
|
||||
|
||||
@@ -51,10 +47,10 @@ jobs:
|
||||
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
|
||||
echo "tinygrad_ref is $ref"
|
||||
|
||||
- name: Checkout docs repo (gh-pages)
|
||||
- name: Checkout docs repo (sunnypilot-models, gh-pages)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.docs_repo }}
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
@@ -82,7 +78,6 @@ jobs:
|
||||
- name: Get next recompiled dir number
|
||||
id: create-recompiled-dir
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
HF_REPO: ${{ github.event.inputs.hf_repo }}
|
||||
run: |
|
||||
pip install huggingface_hub
|
||||
@@ -122,7 +117,6 @@ jobs:
|
||||
json_version: ${{ needs.setup.outputs.json_version }}
|
||||
target_hardware: ${{ github.event.inputs.target_hardware }}
|
||||
hf_repo: ${{ github.event.inputs.hf_repo }}
|
||||
docs_repo: ${{ inputs.docs_repo }}
|
||||
set_min_version: ${{ github.event.inputs.set_min_version }}
|
||||
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
|
||||
secrets: inherit
|
||||
@@ -167,7 +161,6 @@ jobs:
|
||||
target_hardware: ${{ github.event.inputs.target_hardware }}
|
||||
artifact_suffix: -retry
|
||||
hf_repo: ${{ github.event.inputs.hf_repo }}
|
||||
docs_repo: ${{ inputs.docs_repo }}
|
||||
set_min_version: ${{ github.event.inputs.set_min_version }}
|
||||
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -30,7 +30,6 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
model_name: ${{ steps.resolve.outputs.model_name }}
|
||||
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
|
||||
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
|
||||
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
|
||||
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
|
||||
@@ -46,24 +45,28 @@ 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"
|
||||
elif [ "${{ inputs.target }}" = "dm" ]; then
|
||||
ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx"
|
||||
HF_DEFAULTS_PATH="models/defaults/dm"
|
||||
NAME="dmonitoring_model ($(git log -1 --format=%cd --date=format:'%B %d, %Y' -- "$ONNX_PATH"))"
|
||||
else
|
||||
NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL; print(DEFAULT_MODEL)")
|
||||
ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx"
|
||||
HF_DEFAULTS_PATH="models/defaults/small"
|
||||
fi
|
||||
|
||||
ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH")
|
||||
TINYGRAD_REF=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)
|
||||
if [ -z "$TINYGRAD_REF" ]; then
|
||||
echo "::error::Failed to resolve tinygrad ref"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SAFE_NAME="${NAME// /-}"
|
||||
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
|
||||
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
|
||||
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -72,8 +75,6 @@ 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:
|
||||
@@ -81,10 +82,9 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Pull small ONNX via LFS
|
||||
- name: Pull ONNX via LFS
|
||||
run: git lfs pull -I "${{ env.SMALL_ONNX }}"
|
||||
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
source /etc/profile
|
||||
@@ -100,35 +100,24 @@ jobs:
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
|
||||
|
||||
- name: Compile small model from ONNX
|
||||
- name: Compile small model with stock compiler
|
||||
run: |
|
||||
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"
|
||||
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: Compile driving warps (stock)
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
|
||||
MODEL_W=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(s[0])")
|
||||
MODEL_H=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(s[1])")
|
||||
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)")
|
||||
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
for CAM in "1928,1208" "1344,760"; do
|
||||
CAM_W=$(echo $CAM | cut -d, -f1)
|
||||
CAM_H=$(echo $CAM | cut -d, -f2)
|
||||
STRIDE_Y_UV=$(python3 -c 'import sys; from openpilot.system.camerad.cameras.nv12_info import get_nv12_info; s,y,u,_=get_nv12_info(int(sys.argv[1]),int(sys.argv[2])); print(f"{s},{y},{u},{s*(y+u)}")' $CAM_W $CAM_H)
|
||||
OUTPUT="${{ github.workspace }}/${{ env.MODELS_DIR }}/driving_warp_${CAM_W}x${CAM_H}_tinygrad.pkl"
|
||||
env ${TG_FLAGS} python3 ${{ github.workspace }}/${{ env.COMPILER }}/compile_warp.py \
|
||||
--frame ${CAM_W},${CAM_H},${STRIDE_Y_UV} \
|
||||
--warp-to ${MODEL_W}x${MODEL_H} \
|
||||
--layout yuv420 \
|
||||
--frames 2 \
|
||||
--output "${OUTPUT}"
|
||||
done
|
||||
|
||||
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: |
|
||||
@@ -146,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
@@ -169,13 +158,13 @@ jobs:
|
||||
- name: Upload small model artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
||||
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||
path: ${{ github.workspace }}/small_output/
|
||||
|
||||
- name: Upload artifact name file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
||||
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||
path: ${{ github.workspace }}/small_output/artifact_name.txt
|
||||
|
||||
- name: Re-enable powersave
|
||||
@@ -189,14 +178,16 @@ jobs:
|
||||
if: ${{ inputs.target == 'big' }}
|
||||
runs-on: [self-hosted, chestnut]
|
||||
env:
|
||||
MODELS_DIR: openpilot/selfdrive/modeld/models
|
||||
COMPILER: tinygrad_repo/examples/openpilot
|
||||
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Pull big ONNX via LFS
|
||||
run: git lfs pull -I "${{ env.BIG_ONNX }}"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
source /etc/profile
|
||||
@@ -228,25 +219,24 @@ jobs:
|
||||
raise RuntimeError('Chestnut PCIe link not ready after 10 attempts')
|
||||
"
|
||||
|
||||
- name: Compile driving warps (chestnut)
|
||||
- name: Compile big model with stock compiler
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
|
||||
MODEL_W=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(s[0])")
|
||||
MODEL_H=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(s[1])")
|
||||
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"
|
||||
for CAM in "1928,1208" "1344,760"; do
|
||||
CAM_W=$(echo $CAM | cut -d, -f1)
|
||||
CAM_H=$(echo $CAM | cut -d, -f2)
|
||||
STRIDE_Y_UV=$(python3 -c 'import sys; from openpilot.system.camerad.cameras.nv12_info import get_nv12_info; s,y,u,_=get_nv12_info(int(sys.argv[1]),int(sys.argv[2])); print(f"{s},{y},{u},{s*(y+u)}")' $CAM_W $CAM_H)
|
||||
OUTPUT="${{ github.workspace }}/${{ env.MODELS_DIR }}/big_driving_warp_${CAM_W}x${CAM_H}_tinygrad.pkl"
|
||||
env ${TG_FLAGS} python3 ${{ github.workspace }}/${{ env.COMPILER }}/compile_warp.py \
|
||||
--frame ${CAM_W},${CAM_H},${STRIDE_Y_UV} \
|
||||
--warp-to ${MODEL_W}x${MODEL_H} \
|
||||
--layout yuv420 \
|
||||
--frames 2 \
|
||||
--output "${OUTPUT}"
|
||||
done
|
||||
|
||||
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: |
|
||||
@@ -264,7 +254,7 @@ jobs:
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
@@ -287,13 +277,13 @@ jobs:
|
||||
- name: Upload big model artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
||||
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||
path: ${{ github.workspace }}/big_output/
|
||||
|
||||
- name: Upload artifact name file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
||||
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||
path: ${{ github.workspace }}/big_output/artifact_name.txt
|
||||
|
||||
- name: Re-enable powersave
|
||||
@@ -318,6 +308,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Pull ONNX via LFS
|
||||
run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}"
|
||||
|
||||
- name: Install huggingface_hub
|
||||
@@ -327,7 +318,7 @@ jobs:
|
||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
||||
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||
path: artifact_name
|
||||
|
||||
- name: Read artifact name
|
||||
@@ -347,7 +338,7 @@ jobs:
|
||||
- name: Upload model to HF
|
||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
|
||||
run: |
|
||||
rm -f output/artifact_name.txt
|
||||
@@ -357,6 +348,8 @@ jobs:
|
||||
--hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \
|
||||
--artifact-name "$ARTIFACT_NAME" \
|
||||
--model-dir output \
|
||||
--onnx-path "${{ needs.resolve.outputs.onnx_path }}" \
|
||||
--onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \
|
||||
--model-name "${{ needs.resolve.outputs.model_name }}" \
|
||||
--tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \
|
||||
--run-number "${{ github.run_number }}"
|
||||
@@ -371,7 +364,7 @@ jobs:
|
||||
- name: Generate DM metadata and upload to HF
|
||||
if: ${{ inputs.target == 'dm' }}
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
python3 -c "
|
||||
@@ -426,6 +419,8 @@ jobs:
|
||||
--hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \
|
||||
--artifact-name "dm-model-${{ github.run_number }}" \
|
||||
--model-dir dm_output \
|
||||
--onnx-path "${{ needs.resolve.outputs.onnx_path }}" \
|
||||
--onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \
|
||||
--model-name "${{ needs.resolve.outputs.model_name }}" \
|
||||
--tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \
|
||||
--run-number "${{ github.run_number }}"
|
||||
@@ -435,12 +430,14 @@ jobs:
|
||||
if: ${{ inputs.target == 'dm' }}
|
||||
runs-on: [self-hosted, tici]
|
||||
env:
|
||||
DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx
|
||||
DM_PKL: openpilot/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Pull DM ONNX via LFS
|
||||
run: git lfs pull -I "${{ env.DM_ONNX }}"
|
||||
|
||||
- name: Set environment variables
|
||||
@@ -484,29 +481,11 @@ jobs:
|
||||
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"
|
||||
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} \
|
||||
--warp-to ${DM_SIZE} \
|
||||
--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 }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/
|
||||
|
||||
- name: Upload DM artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -39,11 +39,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
docs_repo:
|
||||
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot-models'
|
||||
set_min_version:
|
||||
description: 'Minimum selector version'
|
||||
required: false
|
||||
@@ -112,11 +107,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
docs_repo:
|
||||
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot-models'
|
||||
env:
|
||||
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
|
||||
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
|
||||
@@ -146,7 +136,7 @@ jobs:
|
||||
- name: Checkout docs repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.docs_repo }}
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
@@ -156,7 +146,7 @@ jobs:
|
||||
|
||||
- name: Validate hf_repo and JSON version
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
|
||||
run: |
|
||||
if [ ! -f "$JSON_FILE" ]; then
|
||||
echo "JSON file $JSON_FILE does not exist!"
|
||||
@@ -165,8 +155,13 @@ jobs:
|
||||
python3 -c "
|
||||
import sys
|
||||
from huggingface_hub import HfApi
|
||||
HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset')
|
||||
print(f'Success: Repo {sys.argv[1]} exists.')
|
||||
try:
|
||||
api = HfApi()
|
||||
api.repo_info(repo_id=sys.argv[1], repo_type='dataset')
|
||||
print(f'Success: Repo {sys.argv[1]} exists.')
|
||||
except Exception as e:
|
||||
print('HF validation failed:', e)
|
||||
sys.exit(1)
|
||||
" "${{ inputs.hf_repo }}"
|
||||
|
||||
- name: Download artifact name file
|
||||
@@ -197,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Upload to Hugging Face
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
|
||||
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
||||
run: |
|
||||
hf upload ${{ inputs.hf_repo }} \
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
name: Compile warp for sunnypilot modeld
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
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
|
||||
NV12_INFO=$(python3 -c "from openpilot.system.camerad.cameras.nv12_info import get_nv12_info; w, h = map(int, '${res}'.split('x')); print(','.join(map(str, get_nv12_info(w, 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 tinygrad_repo/examples/openpilot/compile_warp.py \
|
||||
--frame ${res/x/,},${NV12_INFO} --warp-to ${MODEL_SIZE} --layout yuv420 --frames 2 --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 DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32 python3 tinygrad_repo/examples/openpilot/compile_warp.py \
|
||||
--frame ${res/x/,},${NV12_INFO} --warp-to ${MODEL_SIZE} --layout yuv420 --frames 2 --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,25 +18,24 @@ concurrency:
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: build docs
|
||||
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"]') }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: commaai/timeout@v1
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- run: ./tools/op.sh setup
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
# Build
|
||||
- name: Build docs
|
||||
run: ./tools/op.sh docs --build
|
||||
run: |
|
||||
git lfs pull
|
||||
python docs/serve.py --build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -46,13 +46,6 @@ runs:
|
||||
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"
|
||||
|
||||
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
|
||||
fi
|
||||
}
|
||||
|
||||
echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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_tinygrad.pkl
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
build___nightly:
|
||||
@@ -34,7 +34,3 @@ 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_tinygrad.pkl
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
package_updates:
|
||||
|
||||
@@ -103,27 +103,25 @@ jobs:
|
||||
- run: |
|
||||
cd ${{ github.workspace }}/openpilot/openpilot
|
||||
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
|
||||
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx,**/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
|
||||
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
|
||||
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
|
||||
else
|
||||
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/big_*.pkl" -X ""
|
||||
find selfdrive/modeld/models -type f \( -name "*.onnx" -o -name "*.pkl" \) ! -name "big_*.onnx" ! -name "big_*.pkl" -delete
|
||||
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
|
||||
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
|
||||
fi
|
||||
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx selfdrive/modeld/models/*.pkl 2>/dev/null; then
|
||||
echo "::error::the ONNX or PKL files above are still LFS pointers, not real models"
|
||||
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
|
||||
echo "::error::the ONNX files above are still LFS pointers, not real models"
|
||||
exit 1
|
||||
fi
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||
path: |
|
||||
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
|
||||
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.pkl
|
||||
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
|
||||
if-no-files-found: error
|
||||
|
||||
build_model:
|
||||
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
|
||||
runs-on: [self-hosted, chestnut]
|
||||
needs: get_model
|
||||
env:
|
||||
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
||||
@@ -190,7 +188,7 @@ jobs:
|
||||
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 WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||
else
|
||||
echo "QCOM build"
|
||||
@@ -198,75 +196,64 @@ jobs:
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
||||
fi
|
||||
|
||||
NATIVE_PKL=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "*.pkl" -print -quit)
|
||||
# Generate metadata for all ONNX files
|
||||
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
|
||||
echo "Generating metadata: $onnx_file"
|
||||
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
||||
done
|
||||
|
||||
if [ -n "$NATIVE_PKL" ]; then
|
||||
echo "Found native precompiled pkl: $NATIVE_PKL"
|
||||
if [ "$NATIVE_PKL" != "$OUTPUT_PKL" ]; then
|
||||
mv "$NATIVE_PKL" "$OUTPUT_PKL"
|
||||
# Detect model type and build compile args
|
||||
VISION_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
|
||||
[ -f "$f" ] && VISION_ONNX="$f" && break
|
||||
done
|
||||
|
||||
POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
|
||||
[ -f "$f" ] && POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
OFF_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
|
||||
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
ON_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
|
||||
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
SUPERCOMBO_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
|
||||
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
|
||||
done
|
||||
|
||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||
if [ -f "$VISION_ONNX" ]; then
|
||||
ONNX_ARGS="--vision-onnx $VISION_ONNX"
|
||||
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
|
||||
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
|
||||
elif [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
|
||||
fi
|
||||
echo "Chunking pkl"
|
||||
python3 -c "from openpilot.common.file_chunker import chunk_file, get_chunk_targets; import os; p='$OUTPUT_PKL'; chunk_file(p, get_chunk_targets(p, os.path.getsize(p)))"
|
||||
else
|
||||
# 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
|
||||
elif [ -f "$SUPERCOMBO_ONNX" ]; then
|
||||
MODEL_TYPE=supercombo
|
||||
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
|
||||
fi
|
||||
|
||||
# Detect model type and build compile args
|
||||
VISION_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
|
||||
[ -f "$f" ] && VISION_ONNX="$f" && break
|
||||
done
|
||||
|
||||
POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
|
||||
[ -f "$f" ] && POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
OFF_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
|
||||
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
ON_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
|
||||
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
SUPERCOMBO_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
|
||||
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
|
||||
done
|
||||
|
||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||
if [ -f "$VISION_ONNX" ]; then
|
||||
ONNX_ARGS="--vision-onnx $VISION_ONNX"
|
||||
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
|
||||
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_multi_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
|
||||
elif [ -f "$POLICY_ONNX" ]; then
|
||||
MODEL_TYPE=vision_policy
|
||||
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
|
||||
fi
|
||||
elif [ -f "$SUPERCOMBO_ONNX" ]; then
|
||||
MODEL_TYPE=supercombo
|
||||
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
|
||||
fi
|
||||
|
||||
if [ -n "$MODEL_TYPE" ]; then
|
||||
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
|
||||
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
|
||||
--model-type $MODEL_TYPE \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
$ONNX_ARGS \
|
||||
--output "$OUTPUT_PKL"
|
||||
fi
|
||||
if [ -n "$MODEL_TYPE" ]; then
|
||||
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
|
||||
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
|
||||
--model-type $MODEL_TYPE \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
$ONNX_ARGS \
|
||||
--output "$OUTPUT_PKL"
|
||||
fi
|
||||
|
||||
- name: Prepare Output
|
||||
|
||||
@@ -216,9 +216,6 @@ jobs:
|
||||
needs: [ prepare_strategy ]
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
|
||||
concurrency:
|
||||
group: prepare-chestnut
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
|
||||
env:
|
||||
@@ -226,13 +223,14 @@ jobs:
|
||||
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||
HF_DEFAULTS_PATH: models/defaults/big
|
||||
steps:
|
||||
- name: Resolve tinygrad ref via API
|
||||
- name: Resolve ONNX hash and 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_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}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "ONNX hash: $ONNX_HASH"
|
||||
echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||
echo "tinygrad ref: $TINYGRAD_REF"
|
||||
@@ -240,7 +238,7 @@ jobs:
|
||||
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
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
@@ -248,41 +246,24 @@ jobs:
|
||||
}
|
||||
|
||||
if check_defaults; then
|
||||
echo "HF defaults match repo tinygrad ref"
|
||||
echo "HF defaults match repo ONNX hash and tinygrad ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "No matching model on HF — dispatching build"
|
||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=big
|
||||
sleep 10
|
||||
|
||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
||||
|
||||
echo "Waiting for build run to complete..."
|
||||
echo "Polling HF for big model availability..."
|
||||
for i in $(seq 1 90); do
|
||||
sleep 30
|
||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
||||
echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION"
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "Build run succeeded, verifying HF..."
|
||||
sleep 10
|
||||
if check_defaults; then
|
||||
echo "Big model verified on HF"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Build succeeded but model not found on HF"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
||||
exit 1
|
||||
fi
|
||||
if check_defaults; then
|
||||
echo "Big model available on HF after $((i * 30))s"
|
||||
exit 0
|
||||
fi
|
||||
echo "Poll $i/90: not yet available"
|
||||
done
|
||||
|
||||
echo "::error::Build run did not complete within 45 minutes"
|
||||
echo "::error::Big model not available on HF after 45 minutes"
|
||||
exit 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -296,9 +277,6 @@ jobs:
|
||||
prepare_small_model:
|
||||
needs: [ prepare_strategy ]
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: prepare-small-model
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
|
||||
env:
|
||||
@@ -306,12 +284,14 @@ jobs:
|
||||
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||
HF_DEFAULTS_PATH: models/defaults/small
|
||||
steps:
|
||||
- name: Resolve tinygrad ref via API
|
||||
- name: Resolve ONNX hash and 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/driving_supercombo.onnx?ref=${REF}" --jq '.sha')
|
||||
DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "Driving ONNX hash: $DRIVING_HASH"
|
||||
echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||
echo "tinygrad ref: $TINYGRAD_REF"
|
||||
@@ -319,7 +299,7 @@ jobs:
|
||||
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
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
@@ -327,41 +307,24 @@ jobs:
|
||||
}
|
||||
|
||||
if check_defaults; then
|
||||
echo "HF defaults match repo tinygrad ref"
|
||||
echo "HF defaults match repo ONNX hash and tinygrad ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "No matching model on HF — dispatching build"
|
||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=small
|
||||
sleep 10
|
||||
|
||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
||||
|
||||
echo "Waiting for build run to complete..."
|
||||
echo "Polling HF for model availability..."
|
||||
for i in $(seq 1 60); do
|
||||
sleep 30
|
||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
||||
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "Build run succeeded, verifying HF..."
|
||||
sleep 10
|
||||
if check_defaults; then
|
||||
echo "Small model verified on HF"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Build succeeded but model not found on HF"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
||||
exit 1
|
||||
fi
|
||||
if check_defaults; then
|
||||
echo "Model available on HF after $((i * 30))s"
|
||||
exit 0
|
||||
fi
|
||||
echo "Poll $i/60: not yet available"
|
||||
done
|
||||
|
||||
echo "::error::Small model build did not complete within 30 minutes"
|
||||
echo "::error::Small driving model not available on HF after 30 minutes"
|
||||
exit 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -375,9 +338,6 @@ jobs:
|
||||
prepare_dm_model:
|
||||
needs: [ prepare_strategy ]
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: prepare-dm-model
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
|
||||
env:
|
||||
@@ -385,12 +345,14 @@ jobs:
|
||||
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||
HF_DEFAULTS_PATH: models/defaults/dm
|
||||
steps:
|
||||
- name: Resolve tinygrad ref via API
|
||||
- name: Resolve ONNX hash and 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/dmonitoring_model.onnx?ref=${REF}" --jq '.sha')
|
||||
DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "DM ONNX hash: $DM_HASH"
|
||||
echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||
echo "tinygrad ref: $TINYGRAD_REF"
|
||||
@@ -398,7 +360,7 @@ jobs:
|
||||
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
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
@@ -406,41 +368,24 @@ jobs:
|
||||
}
|
||||
|
||||
if check_defaults; then
|
||||
echo "HF defaults match DM tinygrad ref"
|
||||
echo "HF defaults match DM ONNX hash and tinygrad ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "No matching DM model on HF — dispatching build"
|
||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
|
||||
sleep 10
|
||||
|
||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
||||
|
||||
echo "Waiting for build run to complete..."
|
||||
echo "Polling HF for DM model availability..."
|
||||
for i in $(seq 1 60); do
|
||||
sleep 30
|
||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
||||
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "Build run succeeded, verifying HF..."
|
||||
sleep 10
|
||||
if check_defaults; then
|
||||
echo "DM model verified on HF"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Build succeeded but DM model not found on HF"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
||||
exit 1
|
||||
fi
|
||||
if check_defaults; then
|
||||
echo "DM model available on HF after $((i * 30))s"
|
||||
exit 0
|
||||
fi
|
||||
echo "Poll $i/60: not yet available"
|
||||
done
|
||||
|
||||
echo "::error::DM model build did not complete within 30 minutes"
|
||||
echo "::error::DM model not available on HF after 30 minutes"
|
||||
exit 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Debug Discourse Posting
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
test-discourse-post:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Post test message to Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
|
||||
topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
|
||||
message: |
|
||||
## 🧪 Test Post from GitHub Actions
|
||||
|
||||
**This is a test post to verify Discourse integration**
|
||||
|
||||
- **Workflow**: ${{ github.workflow }}
|
||||
- **Run Number**: #${{ github.run_number }}
|
||||
- **Branch**: `${{ github.ref_name }}`
|
||||
- **Commit**: ${{ github.sha }}
|
||||
- **Actor**: @${{ github.actor }}
|
||||
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
---
|
||||
|
||||
### Fake Build Info (for testing)
|
||||
- **Version**: 0.9.8-test
|
||||
- **Build**: #42
|
||||
- **Branch**: release-test
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This is an automated test message. Drive safe! 🚗💨*
|
||||
|
||||
|
||||
- name: Create topic on Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
|
||||
#topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
|
||||
category-id: 4
|
||||
title: "This is a test of a new topic instead of a reply"
|
||||
message: |
|
||||
## 🧪 Test Post from GitHub Actions
|
||||
|
||||
**This is a test post to verify Discourse integration**
|
||||
|
||||
- **Workflow**: ${{ github.workflow }}
|
||||
- **Run Number**: #${{ github.run_number }}
|
||||
- **Branch**: `${{ github.ref_name }}`
|
||||
- **Commit**: ${{ github.sha }}
|
||||
- **Actor**: @${{ github.actor }}
|
||||
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
---
|
||||
|
||||
### Fake Build Info (for testing)
|
||||
- **Version**: 0.9.8-test
|
||||
- **Build**: #42
|
||||
- **Branch**: release-test
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This is an automated test message. Drive safe! 🚗💨*
|
||||
- name: Display results
|
||||
if: always()
|
||||
run: |
|
||||
echo "::notice::Discourse post test completed"
|
||||
echo "Check your Discourse topic to verify the post appeared correctly"
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Test Models Compatibility With Tinygrad Changes
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'tinygrad_repo'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
models: ${{ steps.set-matrix.outputs.models }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch and Parse json
|
||||
id: set-matrix
|
||||
run: |
|
||||
python3 -c '
|
||||
import json, urllib.request, os, re
|
||||
|
||||
with open("openpilot/sunnypilot/models/fetcher.py", "r") as f:
|
||||
urls = re.findall(r"MODEL_URL(?:_CHESTNUT)?\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", f.read())
|
||||
|
||||
artifacts = []
|
||||
for url in urls:
|
||||
data = json.loads(urllib.request.urlopen(url).read())
|
||||
for bundle in data.get("bundles", []):
|
||||
for model in bundle.get("models", []):
|
||||
if "artifact" in model:
|
||||
artifacts.append(model["artifact"])
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
||||
f.write(f"models={json.dumps(artifacts)}\n")
|
||||
'
|
||||
|
||||
test-model:
|
||||
name: Test ${{ matrix.artifact.file_name }}
|
||||
needs: generate-matrix
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/commaai/openpilot-base:latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
artifact: ${{ fromJson(needs.generate-matrix.outputs.models) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Download Model Chunks in Parallel
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
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"]
|
||||
hf_token = os.environ.get("HF_TOKEN", "")
|
||||
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")
|
||||
if hf_token:
|
||||
f.write(f"header = \"Authorization: Bearer {hf_token}\"\n")
|
||||
'
|
||||
curl -Z --parallel-immediate --parallel-max 12 --retry 3 --retry-all-errors -s -S -f -L -K /tmp/curl_config.txt
|
||||
|
||||
- name: Run Model Compatibility Test
|
||||
env:
|
||||
MODEL_BASE_NAME: ${{ matrix.artifact.file_name }}
|
||||
MODEL_CHUNK_DIR: "/tmp/model_chunks"
|
||||
PYTHONPATH: ".:./tinygrad_repo"
|
||||
run: |
|
||||
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_tinygrad.pkl
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
build_release:
|
||||
|
||||
@@ -50,8 +50,6 @@ st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
|
||||
*.stats
|
||||
*.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,4 +19,3 @@
|
||||
[submodule "sunnypilot/neural_network_data"]
|
||||
path = openpilot/sunnypilot/neural_network_data
|
||||
url = https://github.com/sunnypilot/neural-network-data.git
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[lfs]
|
||||
url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs
|
||||
pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git
|
||||
|
||||
locksverify = false
|
||||
|
||||
Vendored
+4
-20
@@ -12,17 +12,16 @@ 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=no -o ControlPath=none -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=12 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec setpriv --pdeathsig HUP /usr/bin/bash <<'END'
|
||||
ssh -o ControlMaster=auto -o ControlPath=/tmp/ssh_control_%C -o ControlPersist=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END'
|
||||
|
||||
set -e
|
||||
|
||||
export TERM=xterm-256color
|
||||
|
||||
trap 'kill 0' HUP # stop this process group on SSH disconnect
|
||||
shopt -s huponexit # kill all child processes when the shell exits
|
||||
|
||||
export CI=1
|
||||
export 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}
|
||||
@@ -70,8 +69,7 @@ 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} ) &
|
||||
wait \$!
|
||||
time ${cmd}
|
||||
END"""
|
||||
|
||||
sh script: ssh_cmd, label: step_label
|
||||
@@ -171,7 +169,7 @@ node {
|
||||
env.GIT_BRANCH = checkout(scm).GIT_BRANCH
|
||||
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
|
||||
|
||||
def excludeBranches = ['__nightly', '__nightly-chestnut', 'devel', 'devel-staging',
|
||||
def excludeBranches = ['__nightly', 'devel', 'devel-staging',
|
||||
'release-tizi', 'release-tizi-staging', 'release-mici', 'release-mici-staging', 'testing-closet*', 'hotfix-*']
|
||||
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
|
||||
|
||||
@@ -203,12 +201,6 @@ node {
|
||||
)
|
||||
}
|
||||
|
||||
if (env.BRANCH_NAME == '__nightly-chestnut') {
|
||||
deviceStage("build nightly-chestnut", "mici-chestnut-ci", [], [
|
||||
step("build nightly-chestnut", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-chestnut $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"),
|
||||
])
|
||||
}
|
||||
|
||||
if (!env.BRANCH_NAME.matches(excludeRegex)) {
|
||||
parallel (
|
||||
'onroad tests': {
|
||||
@@ -260,14 +252,6 @@ node {
|
||||
step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"),
|
||||
])
|
||||
},
|
||||
'chestnut': {
|
||||
deviceStage("chestnut", "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]),
|
||||
step("test power draw", "./openpilot/selfdrive/test/test_power_draw.py"),
|
||||
])
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
+1
-2
@@ -87,6 +87,7 @@ acados_include_dirs = [
|
||||
# vendored in commaai/dependencies.
|
||||
allowed_system_libs = {
|
||||
"EGL", "GLESv2", "GL",
|
||||
"Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
|
||||
"dl", "drm", "gbm", "m", "pthread",
|
||||
}
|
||||
|
||||
@@ -345,8 +346,6 @@ 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 str(t).endswith('.pkl'): # chunked during release packaging
|
||||
continue
|
||||
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'):
|
||||
|
||||
+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|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>|||
|
||||
|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>|||
|
||||
|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-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|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|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-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>|||
|
||||
|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>|||
|
||||
|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|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)|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[<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>Model years 2023 and 2024 can have either hardware type, depending on build date and factory. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>9</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>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
|
||||
op docs --build
|
||||
python docs/serve.py --build
|
||||
```
|
||||
|
||||
**2. Run the site locally** (rebuilds on change)
|
||||
``` bash
|
||||
op docs
|
||||
python docs/serve.py
|
||||
```
|
||||
|
||||
+10
-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,9 @@ 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
|
||||
|
||||
echo "Restarting launch script ${LAUNCHER_LOCATION}"
|
||||
unset AGNOS_VERSION
|
||||
@@ -69,7 +69,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 +90,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.6"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: b4fcf1aa84...06743dfb39
@@ -353,7 +353,6 @@ struct OnroadEventSP @0xda96579883444c35 {
|
||||
speedLimitPending @22;
|
||||
e2eChime @23;
|
||||
laneChangeRoadEdge @24;
|
||||
bigModelReady @25;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -725,7 +725,6 @@ struct ChestnutState {
|
||||
pcieLtssm @7 :UInt8;
|
||||
supplyVoltage @8 :UInt16; # mV
|
||||
supplyCurrent @9 :Int16; # mA
|
||||
supplyFault @10 :Bool;
|
||||
}
|
||||
|
||||
struct RadarState @0x9a185389d6fdd05f {
|
||||
@@ -1005,7 +1004,6 @@ struct DrivingModelData {
|
||||
frameIdExtra @1 :UInt32;
|
||||
frameDropPerc @6 :Float32;
|
||||
modelExecutionTime @7 :Float32;
|
||||
big @8 :Bool;
|
||||
|
||||
action @2 :ModelDataV2.Action;
|
||||
|
||||
@@ -2594,7 +2592,6 @@ struct Event {
|
||||
clocks @35 :Clocks;
|
||||
deviceState @6 :DeviceState;
|
||||
chestnutState @152 :ChestnutState;
|
||||
chestnutGpuState @153 :ChestnutState;
|
||||
logMessage @18 :Text;
|
||||
errorLogMessage @85 :Text;
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ _services: dict[str, tuple] = {
|
||||
"accelerometer": (True, 104., 104),
|
||||
"temperatureSensor": (True, 2., 200),
|
||||
"deviceState": (True, 2., 1),
|
||||
"chestnutState": (True, 10., 1),
|
||||
"chestnutGpuState": (False, 10.),
|
||||
"chestnutState": (True, 10., 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),
|
||||
|
||||
@@ -21,13 +21,6 @@ class Profile:
|
||||
def is_comma(self) -> bool:
|
||||
return self.provider == 'Webbing' and self.iccid.startswith('8985235')
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
if self.is_comma:
|
||||
return "comma prime"
|
||||
name = self.nickname or self.provider or "<unnamed>"
|
||||
return f"{name} (...{self.iccid[-4:]})"
|
||||
|
||||
|
||||
class LPABase(ABC):
|
||||
@abstractmethod
|
||||
|
||||
@@ -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 or not all(parts):
|
||||
if len(parts) != 3:
|
||||
raise ValueError("Invalid activation code format")
|
||||
return parts[1], parts[2]
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ 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:
|
||||
@@ -32,6 +31,14 @@ 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)
|
||||
@@ -59,11 +66,11 @@ class ChunkStream(io.RawIOBase):
|
||||
|
||||
def open_file_chunked(path):
|
||||
manifest_path = get_manifest_path(path)
|
||||
if os.path.isfile(path):
|
||||
paths = [path]
|
||||
elif os.path.isfile(manifest_path):
|
||||
if os.path.isfile(manifest_path):
|
||||
num_chunks = int(Path(manifest_path).read_text().strip())
|
||||
paths = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
|
||||
elif os.path.isfile(path):
|
||||
paths = [path]
|
||||
else:
|
||||
raise FileNotFoundError(path)
|
||||
return io.BufferedReader(ChunkStream(paths))
|
||||
|
||||
@@ -145,9 +145,6 @@ class HardwareBase(ABC):
|
||||
def get_modem_temperatures(self):
|
||||
return []
|
||||
|
||||
def get_modem_state(self) -> dict:
|
||||
return {}
|
||||
|
||||
def initialize_hardware(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -56,29 +56,29 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
|
||||
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"size": 46897152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
|
||||
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
|
||||
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
|
||||
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
|
||||
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||
"alt": {
|
||||
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
|
||||
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import string
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -355,7 +354,7 @@ class Modem:
|
||||
imei = ""
|
||||
|
||||
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
||||
if not all(c in string.hexdigits for c in iccid):
|
||||
if not iccid.isdigit():
|
||||
iccid = ""
|
||||
|
||||
imsi = first_line("AT+CIMI")
|
||||
|
||||
@@ -9,25 +9,22 @@ from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
|
||||
|
||||
def read_power(panda=None):
|
||||
if panda is not None and panda.get_type() == panda.HW_TYPE_CUATRO:
|
||||
health = panda.health()
|
||||
return health['voltage'] * health['current'] / 1e6
|
||||
def read_power():
|
||||
with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f:
|
||||
return int(f.read()) / 1e6
|
||||
|
||||
def sample_power(seconds=5, panda=None) -> list[float]:
|
||||
def sample_power(seconds=5) -> list[float]:
|
||||
rate = 123
|
||||
rk = Ratekeeper(rate, print_delay_threshold=None)
|
||||
|
||||
pwrs = []
|
||||
for _ in range(rate*seconds):
|
||||
pwrs.append(read_power(panda))
|
||||
pwrs.append(read_power())
|
||||
rk.keep_time()
|
||||
return pwrs
|
||||
|
||||
def get_power(seconds=5, panda=None):
|
||||
pwrs = sample_power(seconds, panda)
|
||||
def get_power(seconds=5):
|
||||
pwrs = sample_power(seconds)
|
||||
return np.mean(pwrs)
|
||||
|
||||
def wait_for_power(min_pwr, max_pwr, min_secs_in_range, timeout):
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6a7adb302d378dda7b1788a841b89e4f905c872550a70a76650dcde977b4ece0
|
||||
size 24709209
|
||||
oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
|
||||
size 24709205
|
||||
|
||||
@@ -4,17 +4,11 @@ from pathlib import Path
|
||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
|
||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
||||
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
||||
|
||||
|
||||
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
|
||||
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
|
||||
return (vendor_id, product_id) in ids
|
||||
|
||||
|
||||
def get_usb_topology() -> set[str]:
|
||||
try:
|
||||
return set(os.listdir(USB_DEVICES_PATH))
|
||||
@@ -87,7 +81,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
||||
entry.linkErrorCount = device["linkErrorCount"]
|
||||
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
||||
|
||||
if is_chestnut_usb_id(entry.vendorId, entry.productId):
|
||||
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
|
||||
chestnut_present = True
|
||||
|
||||
device_state.chestnutPresent = chestnut_present
|
||||
|
||||
@@ -28,7 +28,6 @@ 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"}},
|
||||
@@ -93,12 +92,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
||||
|
||||
@@ -27,14 +27,14 @@ public:
|
||||
auto param_path = Params().getParamPath();
|
||||
if (util::file_exists(param_path)) {
|
||||
std::string real_path = util::readlink(param_path);
|
||||
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
|
||||
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
|
||||
unlink(param_path.c_str());
|
||||
}
|
||||
if (getenv("COMMA_CACHE") == nullptr) {
|
||||
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
|
||||
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
|
||||
}
|
||||
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
|
||||
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
|
||||
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
|
||||
unsetenv("OPENPILOT_PREFIX");
|
||||
}
|
||||
|
||||
|
||||
+35
-556
@@ -1,47 +1,16 @@
|
||||
"""QR code encoding, decoding, and UI textures."""
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
"""Small QR encoder for the UI's byte-mode, error-correction-level-L codes."""
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
|
||||
# (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)),
|
||||
]
|
||||
# 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)
|
||||
|
||||
# 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)]
|
||||
# 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412
|
||||
_FORMAT_BITS = 0b111011111000100
|
||||
|
||||
|
||||
def _raw_modules(version: int) -> int:
|
||||
@@ -52,24 +21,8 @@ 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 sum(_block_lengths(version, 0))
|
||||
return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version]
|
||||
|
||||
|
||||
def _append_bits(bits: list[int], value: int, length: int) -> None:
|
||||
@@ -96,18 +49,37 @@ 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)
|
||||
divisor = _divisor(_EC[version - 1][0][0])
|
||||
blocks = []
|
||||
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]] = []
|
||||
offset = 0
|
||||
for length in _block_lengths(version, 0):
|
||||
for i in range(num_blocks):
|
||||
length = short_len - ecc_len + (0 if i < num_short else 1)
|
||||
block = data[offset:offset + length]
|
||||
blocks.append(block + _remainder(block, divisor))
|
||||
blocks.append((block, _remainder(block, divisor)))
|
||||
offset += length
|
||||
return bytes(blocks[b][i] for b, i in _interleaved(version, 0))
|
||||
result = bytearray()
|
||||
for i in range(short_len - ecc_len + 1):
|
||||
for block, _ in blocks:
|
||||
result.extend(block[i:i + 1])
|
||||
for i in range(ecc_len):
|
||||
for _, ecc in blocks:
|
||||
result.append(ecc[i])
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def _multiply(x: int, y: int) -> int:
|
||||
return _EXP[(_LOG[x] + _LOG[y]) % 255] if x and y else 0
|
||||
result = 0
|
||||
for _ in range(8):
|
||||
result = (result << 1) ^ (0x11D if result & 0x80 else 0)
|
||||
if y & 0x80:
|
||||
result ^= x
|
||||
y <<= 1
|
||||
return result
|
||||
|
||||
|
||||
def _divisor(degree: int) -> bytes:
|
||||
@@ -136,7 +108,7 @@ def _alignment_positions(version: int) -> list[int]:
|
||||
if version == 1:
|
||||
return []
|
||||
count = version // 7 + 2
|
||||
step = (version * 8 + count * 3 + 5) // (count * 4 - 4) * 2
|
||||
step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2
|
||||
return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1]
|
||||
|
||||
|
||||
@@ -199,7 +171,7 @@ class _Qr:
|
||||
|
||||
def _format(self) -> None:
|
||||
for i in range(15):
|
||||
bit = ((_FORMATS[1 << 3 | 0] >> i) & 1) != 0 # level L, mask 0
|
||||
bit = ((_FORMAT_BITS >> i) & 1) != 0
|
||||
y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i
|
||||
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
|
||||
@@ -244,496 +216,3 @@ 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
|
||||
|
||||
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.
@@ -1,178 +0,0 @@
|
||||
import hashlib
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common import qrcode as qr
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
LPA = "LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5"
|
||||
|
||||
|
||||
# Matrices generated with python-qrcode 8.2, covering all versions and EC levels.
|
||||
# Packed fixtures keep the decoder tests independent of our encoder.
|
||||
FIXTURES = {}
|
||||
for path in Path(__file__).with_name("fixtures").glob("qrcode_*.npz"):
|
||||
with np.load(path) as fixtures:
|
||||
FIXTURES.update({key: fixtures[key] for key in fixtures.files})
|
||||
|
||||
|
||||
def fixture(key: str) -> np.ndarray:
|
||||
bits = np.unpackbits(FIXTURES[key])
|
||||
size = math.isqrt(len(bits))
|
||||
return bits[:size * size].reshape(size, size).astype(bool)
|
||||
|
||||
|
||||
def render(matrix: np.ndarray, box: int = 6, border: int = 4) -> np.ndarray:
|
||||
img = np.repeat(np.repeat(np.pad(matrix, border), box, axis=0), box, axis=1)
|
||||
return np.where(img, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def make(data: str, version: int | None = None, level: int = 0, box: int = 6, border: int = 4):
|
||||
matrix = fixture(hashlib.sha256(f"{version}:{level}:{data}".encode()).hexdigest())
|
||||
return matrix, render(matrix, box, border)
|
||||
|
||||
|
||||
def warp(img: np.ndarray, H: np.ndarray) -> np.ndarray:
|
||||
"""Bilinear resampling through the output -> input homography H, white outside the image."""
|
||||
h, w = img.shape
|
||||
rows, cols = np.mgrid[0:h, 0:w]
|
||||
pts = qr._transform(H, np.column_stack((cols.ravel() + 0.5, rows.ravel() + 0.5))) - 0.5
|
||||
x0, y0 = np.floor(pts[:, 0]).astype(int), np.floor(pts[:, 1]).astype(int)
|
||||
fx, fy = pts[:, 0] - x0, pts[:, 1] - y0
|
||||
padded = np.pad(img.astype(float), 1, constant_values=255)
|
||||
|
||||
def at(y, x):
|
||||
return padded[np.clip(y + 1, 0, h + 1), np.clip(x + 1, 0, w + 1)]
|
||||
|
||||
out = at(y0, x0) * (1 - fx) * (1 - fy) + at(y0, x0 + 1) * fx * (1 - fy) + at(y0 + 1, x0) * (1 - fx) * fy + at(y0 + 1, x0 + 1) * fx * fy
|
||||
return np.clip(out, 0, 255).astype(np.uint8).reshape(h, w)
|
||||
|
||||
|
||||
def rotate(img: np.ndarray, angle: float) -> np.ndarray:
|
||||
h, w = img.shape
|
||||
t = np.radians(angle)
|
||||
R = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]])
|
||||
center = np.array([w / 2, h / 2])
|
||||
corners = np.array([(0, 0), (w, 0), (w, h), (0, h)], dtype=float)
|
||||
return warp(img, qr._perspective((corners - center) @ R.T + center, corners))
|
||||
|
||||
|
||||
class TestQRCode(OpenpilotTestCase):
|
||||
def test_alignment_positions(self):
|
||||
assert qr._alignment_positions(7) == [6, 22, 38]
|
||||
assert qr._alignment_positions(32) == [6, 34, 60, 86, 112, 138]
|
||||
assert qr._alignment_positions(40) == [6, 30, 58, 86, 114, 142, 170]
|
||||
|
||||
def test_all_versions(self):
|
||||
for version in range(1, 41):
|
||||
for level in range(4):
|
||||
with self.subTest(version=version, level=level):
|
||||
data = "".join(chr(ord("a") + i % 26) for i in range(version))
|
||||
matrix, img = make(data, version, level, box=3)
|
||||
assert qr.decode_matrix(matrix) == data
|
||||
assert qr.decode(img) == data
|
||||
|
||||
def test_modes(self):
|
||||
for data in ["0123456789012345", "HELLO WORLD $1.50", LPA, "こんにちは", "ünïcødé", "mixed 123 ABC xyz"]:
|
||||
with self.subTest(data=data):
|
||||
matrix, img = make(data)
|
||||
assert qr.decode_matrix(matrix) == data
|
||||
assert qr.decode(img) == data
|
||||
|
||||
def test_error_correction(self):
|
||||
matrix, _ = make(LPA, level=2)
|
||||
rng = np.random.default_rng(0)
|
||||
flipped = matrix.copy()
|
||||
for r, c in rng.integers(9, matrix.shape[0] - 9, size=(40, 2)):
|
||||
flipped[r, c] ^= True
|
||||
assert qr.decode_matrix(flipped) == LPA
|
||||
|
||||
def test_large_modules(self):
|
||||
for data in ["0123456789012345", "HELLO WORLD $1.50", LPA, "mixed 123 ABC xyz"]:
|
||||
for box in [16, 20, 24, 32]:
|
||||
for dark, light in [(0, 255), (60, 200), (140, 250)]:
|
||||
with self.subTest(data=data, box=box, dark=dark):
|
||||
_, img = make(data, box=box)
|
||||
img = np.where(img == 0, dark, light).astype(np.uint8)
|
||||
assert qr.decode(img) == data
|
||||
|
||||
def test_image_edges(self):
|
||||
# a code touching the image edge must not lose the rows and columns left over from tiling
|
||||
matrix, _ = make(LPA)
|
||||
for size in (200, 203):
|
||||
with self.subTest(size=size):
|
||||
img = np.full((size, size), 255, dtype=np.uint8)
|
||||
code = render(matrix, box=5, border=0)
|
||||
img[size - code.shape[0]:, size - code.shape[1]:] = code
|
||||
assert qr.decode(img) == LPA
|
||||
|
||||
def test_eci(self):
|
||||
# qrcode_eci.npz: packed Segno 1.6.6 matrices, generated with mode='byte',
|
||||
# eci=True, micro=False and the named encoding. Mixed also includes numeric,
|
||||
# alphanumeric, and Kanji segments after changing the byte encoding twice.
|
||||
cases = {
|
||||
"iso8859-5": "Привет", "utf-16-be": "héllo", "utf-8": "こんにちは",
|
||||
"shift_jis": "日本語", "cp1251": "Привет", "iso8859-1": "héllo",
|
||||
"mixed": "hélloПривет日本語123ABC漢字",
|
||||
}
|
||||
for encoding, expected in cases.items():
|
||||
with self.subTest(encoding=encoding):
|
||||
matrix = fixture(encoding)
|
||||
assert qr.decode_matrix(matrix) == expected
|
||||
assert qr.decode(render(matrix)) == expected
|
||||
|
||||
def test_parse_data(self):
|
||||
def parse(stream: str) -> str:
|
||||
stream += '0' * (-len(stream) % 8)
|
||||
return qr._parse_data([int(stream[i:i + 8], 2) for i in range(0, len(stream), 8)], 1)
|
||||
|
||||
def eci(assignment: str, payload: bytes = b'A') -> str:
|
||||
return parse('0111' + assignment + '0100' + f'{len(payload):08b}' + ''.join(f'{b:08b}' for b in payload) + '0000')
|
||||
|
||||
# ASCII assignment 170 uses the two-byte ECI representation.
|
||||
assert eci('1000000010101010') == 'A'
|
||||
for assignment in ['00001110', '1000001111100111', '110000010000000000000000', '11100000']:
|
||||
with self.subTest(assignment=assignment), self.assertRaises(qr.QRError):
|
||||
eci(assignment)
|
||||
with self.assertRaises(qr.QRError):
|
||||
eci('00011010', b'\xff') # Invalid UTF-8 must not fall back to Latin-1.
|
||||
|
||||
# out-of-range numeric, alphanumeric, and Kanji values are format errors, not crashes
|
||||
for stream in ['0001' + '0000000011' + '1111111111', '0001' + '0000000010' + '1111111',
|
||||
'0010' + '000000010' + '11111111111', '0010' + '000000001' + '111111',
|
||||
'1000' + '00000001' + '0000000111111']:
|
||||
with self.subTest(stream=stream), self.assertRaises(qr.QRError):
|
||||
parse(stream)
|
||||
|
||||
def test_rotation(self):
|
||||
for angle in [0, 90, 180, 270, 25, 110]:
|
||||
with self.subTest(angle=angle):
|
||||
_, img = make(LPA, box=8, border=12)
|
||||
assert qr.decode(rotate(img, angle)) == LPA
|
||||
|
||||
def test_mirrored(self):
|
||||
_, img = make(LPA)
|
||||
assert qr.decode(img[:, ::-1]) == LPA
|
||||
|
||||
def test_perspective_and_noise(self):
|
||||
_, img = make(LPA, box=10, border=8)
|
||||
h, w = img.shape
|
||||
corners = np.array([(40, 60), (w - 20, 30), (w - 60, h - 40), (30, h - 90)])
|
||||
arr = warp(img, qr._perspective(corners, np.array([(0, 0), (w, 0), (w, h), (0, h)]))).astype(float)
|
||||
rng = np.random.default_rng(1)
|
||||
arr = arr * 0.6 + 60 + rng.normal(0, 12, arr.shape) # low contrast + noise
|
||||
# uneven lighting
|
||||
arr += np.linspace(-40, 40, w)[None, :]
|
||||
assert qr.decode(np.clip(arr, 0, 255).astype(np.uint8)) == LPA
|
||||
|
||||
def test_no_code(self):
|
||||
rng = np.random.default_rng(2)
|
||||
assert qr.decode(rng.integers(0, 256, size=(240, 320), dtype=np.uint8)) is None
|
||||
assert qr.decode(np.full((240, 320), 200, dtype=np.uint8)) is None
|
||||
|
||||
def test_encoder_roundtrip(self):
|
||||
for version in range(1, 21):
|
||||
with self.subTest(version=version):
|
||||
assert qr.decode_matrix(np.array(qr._Qr(version, b"hello").modules)) == "hello"
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf97a6738b294ac0aed9b2d075916cee0b7d3215bcd23381760900ede6a92748
|
||||
size 13256
|
||||
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
|
||||
size 13079
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
|
||||
size 1544
|
||||
@@ -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"
|
||||
|
||||
@@ -186,6 +186,9 @@ class Car:
|
||||
# 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"""
|
||||
|
||||
|
||||
@@ -278,12 +278,12 @@ def main():
|
||||
estimator = LocationEstimator(DEBUG)
|
||||
|
||||
filter_initialized = False
|
||||
critical_services = ["accelerometer", "gyroscope", "cameraOdometry"]
|
||||
critcal_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 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}
|
||||
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}
|
||||
|
||||
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 critical_services:
|
||||
if which not in critcal_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 critical_services)
|
||||
critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critcal_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)
|
||||
|
||||
|
||||
@@ -1,90 +1,147 @@
|
||||
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.helpers import chestnut_present
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
||||
|
||||
|
||||
CAMERA_CONFIGS = [
|
||||
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||
]
|
||||
|
||||
Import('env', 'arch')
|
||||
chunker_file = File("#openpilot/common/file_chunker.py")
|
||||
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))]
|
||||
|
||||
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
|
||||
def estimate_pickle_max_size(onnx_size):
|
||||
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||
|
||||
if arch == 'comma_arm64':
|
||||
tg_flags = 'DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||
tg_backend = 'QCOM'
|
||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||
else:
|
||||
# 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'
|
||||
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.modeld': {
|
||||
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
|
||||
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
|
||||
},
|
||||
'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_MIN_GLOBALS=32'
|
||||
chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
|
||||
# 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,
|
||||
)
|
||||
|
||||
# tinygrad calls brew which needs a $HOME in the env
|
||||
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
|
||||
|
||||
warp_deps = [File("#openpilot/system/camerad/cameras/nv12_info.py")]
|
||||
compiler = Dir('#tinygrad_repo/examples/openpilot').abspath
|
||||
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
||||
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
||||
|
||||
def chestnut_action(command):
|
||||
def do_compile(target, source, env):
|
||||
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 warp build")
|
||||
return
|
||||
return env.Execute(command)
|
||||
return Action(do_compile, " [CHESTNUT] $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}" --device-input "*" --out-of-band --benchmark-runs 1')
|
||||
lenv.Command(
|
||||
target_pkl_path,
|
||||
tinygrad_files + [onnx_path, Value(cmd)],
|
||||
Action(cmd, " [ONNX] $TARGET"),
|
||||
)
|
||||
|
||||
compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygrad.pkl')
|
||||
compile_model('models/driving_supercombo.onnx', 'models/driving_tinygrad.pkl')
|
||||
|
||||
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
|
||||
for chestnut in [False, True] if CHESTNUT else [False]:
|
||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
|
||||
for cam_w, cam_h in camera_configs:
|
||||
warp_pkl_path = File(f"models/{file_prefix}driving_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||
cmd = (f'{cmd_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}')
|
||||
action = chestnut_action(cmd) if chestnut else cmd
|
||||
node = lenv.Command(warp_pkl_path, tinygrad_files + warp_deps + [Value(cmd)], action)
|
||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
|
||||
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
|
||||
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut
|
||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') 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(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
|
||||
for cam_w, cam_h in camera_configs:
|
||||
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
|
||||
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)
|
||||
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")],
|
||||
)
|
||||
|
||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
tg_compile(tg_flags, 'dmonitoring_model')
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/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)
|
||||
Executable
+319
@@ -0,0 +1,319 @@
|
||||
#!/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'])
|
||||
WARP_INPUTS = ['tfm', 'big_tfm']
|
||||
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||
|
||||
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||
|
||||
WARP_DEV = os.getenv('WARP_DEV')
|
||||
|
||||
|
||||
def make_random_images(keys, shape, device=None):
|
||||
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
|
||||
|
||||
|
||||
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=WARP_DEV)
|
||||
# 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 make_warp_input_queues(vision_input_shapes, frame_skip, device):
|
||||
img = vision_input_shapes['img'] # (1, 12, 128, 256)
|
||||
n_frames = img[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
|
||||
npy = {
|
||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
}
|
||||
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(),
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
||||
}
|
||||
return input_queues, npy
|
||||
|
||||
|
||||
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, 24, 512)
|
||||
# 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], fb[2])}
|
||||
return shapes, [math.prod(s) for s in shapes.values()]
|
||||
|
||||
|
||||
def make_input_queues(input_shapes, frame_skip, device):
|
||||
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||
|
||||
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
|
||||
shapes, sizes = get_policy_npy_shapes(input_shapes)
|
||||
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||
# views into the packed inputs, to be refilled at runtime
|
||||
npy.update({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.update({
|
||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), 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_npy_inputs, device='NPY').realize(),
|
||||
})
|
||||
return input_queues, npy
|
||||
|
||||
|
||||
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_skip):
|
||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||
|
||||
def warp(tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEV)
|
||||
big_tfm = big_tfm.to(WARP_DEV)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
|
||||
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'])
|
||||
|
||||
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)
|
||||
warped = warped.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,
|
||||
'desire_pulse': desire_buf,
|
||||
'traffic_convention': traffic_convention,
|
||||
'action_t': action_t,
|
||||
}
|
||||
out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||
return out,
|
||||
return run_policy
|
||||
|
||||
|
||||
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
SEED = 42
|
||||
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy = make_queues(Device.DEFAULT)
|
||||
rng = np.random.default_rng(seed)
|
||||
Tensor.manual_seed(seed)
|
||||
|
||||
testing = test_val is not None or test_buffers is not None
|
||||
n_runs = 1 if testing else 3
|
||||
|
||||
for i in range(n_runs):
|
||||
for v in npy.values():
|
||||
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||
Device.default.synchronize()
|
||||
random_inputs = make_random_inputs()
|
||||
st = time.perf_counter()
|
||||
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
|
||||
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)
|
||||
print('pickle round trip')
|
||||
with tempfile.TemporaryFile(dir=".") as f:
|
||||
dump_oob(jit, f)
|
||||
f.seek(0)
|
||||
jit = load_oob(f)
|
||||
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
|
||||
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
|
||||
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)
|
||||
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)}
|
||||
|
||||
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
|
||||
|
||||
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
|
||||
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
|
||||
make_policy_queues)
|
||||
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
|
||||
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
|
||||
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
|
||||
|
||||
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)")
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import base64
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, load_oob
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, get_tg_input_devices
|
||||
from tinygrad.tensor import Tensor
|
||||
import time
|
||||
import pickle
|
||||
@@ -19,8 +18,10 @@ from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp
|
||||
|
||||
PROCESS_NAME = "openpilot.selfdrive.modeld.dmonitoringmodeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
MODEL_PKL_PATH = MODELS_DIR / 'dmonitoring_model_tinygrad.pkl'
|
||||
METADATA_PATH = MODELS_DIR / 'dmonitoring_model_metadata.pkl'
|
||||
|
||||
|
||||
class ModelState:
|
||||
@@ -28,10 +29,11 @@ class ModelState:
|
||||
output: np.ndarray
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int):
|
||||
jits = load_oob(open_file_chunked(MODEL_PKL_PATH))
|
||||
self.DEV = jits['input_specs']['input_img'][2]
|
||||
self.input_shapes = jits['metadata']['input_shapes']
|
||||
self.output_slices = pickle.loads(base64.b64decode(jits['metadata']['metadata']['output_slices']))
|
||||
self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV']
|
||||
with open(METADATA_PATH, 'rb') as f:
|
||||
model_metadata = pickle.load(f)
|
||||
self.input_shapes = model_metadata['input_shapes']
|
||||
self.output_slices = model_metadata['output_slices']
|
||||
|
||||
self.numpy_inputs = {
|
||||
'calib': np.zeros(self.input_shapes['calib'], dtype=np.float32),
|
||||
@@ -40,19 +42,16 @@ class ModelState:
|
||||
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
|
||||
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
|
||||
self.frame_buf_params = get_nv12_info(cam_w, cam_h)
|
||||
self.tensor_inputs = {k: Tensor(v, device=self.DEV).realize() for k,v in self.numpy_inputs.items()}
|
||||
self.calib_host = Tensor(self.numpy_inputs['calib'], device='NPY')._buffer()
|
||||
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||
self._blob_cache : dict[int, Tensor] = {}
|
||||
self.model_run = jits['run']
|
||||
self.outputs = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize() for name, (shape, dtype, device) in jits['output_specs'].items()}
|
||||
self.model_run = pickle.load(open_file_chunked(str(MODEL_PKL_PATH)))
|
||||
with open(MODELS_DIR / f'dm_warp_{cam_w}x{cam_h}_tinygrad.pkl', "rb") as f:
|
||||
self.image_warp = pickle.load(f)['run']
|
||||
self.image_warp = pickle.load(f)
|
||||
|
||||
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
|
||||
self.numpy_inputs['calib'][0,:] = calib
|
||||
|
||||
t1 = time.perf_counter()
|
||||
self.tensor_inputs['calib']._buffer().copy_from(self.calib_host)
|
||||
|
||||
ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data
|
||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||
@@ -60,10 +59,9 @@ class ModelState:
|
||||
self._blob_cache[ptr] = Tensor.from_blob(ptr, (self.frame_buf_params[3],), dtype='uint8', device=self.DEV)
|
||||
|
||||
self.warp_inputs_np['transform'][:] = transform[:]
|
||||
self.tensor_inputs['input_img'] = self.image_warp(input_frame=self._blob_cache[ptr], M_inv=self.warp_inputs['transform'])
|
||||
self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform'])
|
||||
|
||||
self.model_run(output_buffers=self.outputs, **self.tensor_inputs)
|
||||
output = self.outputs['outputs'].numpy().astype(np.float32).reshape(-1)
|
||||
output = self.model_run(**self.tensor_inputs).numpy().flatten()
|
||||
|
||||
t2 = time.perf_counter()
|
||||
return output, t2 - t1
|
||||
|
||||
@@ -64,7 +64,6 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
|
||||
driving_model_data.frameIdExtra = modelV2.frameIdExtra
|
||||
driving_model_data.frameDropPerc = modelV2.frameDropPerc
|
||||
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
|
||||
driving_model_data.big = modelV2.big
|
||||
driving_model_data.action = modelV2.action
|
||||
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
|
||||
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import pathlib
|
||||
import codecs
|
||||
import pickle
|
||||
from typing import Any
|
||||
|
||||
from tinygrad.nn.onnx import OnnxPBParser
|
||||
|
||||
|
||||
class MetadataOnnxPBParser(OnnxPBParser):
|
||||
def _parse_ModelProto(self) -> dict:
|
||||
obj: dict[str, Any] = {"graph": {"input": [], "output": []}, "metadata_props": []}
|
||||
for fid, wire_type in self._parse_message(self.reader.len):
|
||||
match fid:
|
||||
case 7:
|
||||
obj["graph"] = self._parse_GraphProto()
|
||||
case 14:
|
||||
obj["metadata_props"].append(self._parse_StringStringEntryProto())
|
||||
case _:
|
||||
self.reader.skip_field(wire_type)
|
||||
return obj
|
||||
|
||||
|
||||
def get_name_and_shape(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]:
|
||||
shape = tuple(int(dim) if isinstance(dim, int) else 0 for dim in value_info["parsed_type"].shape)
|
||||
name = value_info["name"]
|
||||
return name, shape
|
||||
|
||||
|
||||
def get_metadata_value_by_name(model: dict[str, Any], name: str) -> str | Any:
|
||||
for prop in model["metadata_props"]:
|
||||
if prop["key"] == name:
|
||||
return prop["value"]
|
||||
return None
|
||||
|
||||
|
||||
def make_metadata_dict(model_path):
|
||||
model = MetadataOnnxPBParser(model_path).parse()
|
||||
output_slices = get_metadata_value_by_name(model, 'output_slices')
|
||||
assert output_slices is not None, 'output_slices not found in metadata'
|
||||
return {
|
||||
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
|
||||
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
|
||||
'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]),
|
||||
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
model_path = pathlib.Path(sys.argv[1])
|
||||
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')
|
||||
with open(metadata_path, 'wb') as f:
|
||||
pickle.dump(make_metadata_dict(model_path), f)
|
||||
print(f'saved metadata to {metadata_path}')
|
||||
@@ -1,25 +1,47 @@
|
||||
import io
|
||||
import json
|
||||
import pickle
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.file_chunker import get_manifest_path
|
||||
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
|
||||
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH
|
||||
|
||||
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
||||
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
||||
|
||||
|
||||
def get_tg_input_devices(process_name: str, chestnut: bool):
|
||||
with open(TG_INPUT_DEVICES_PATH) as f:
|
||||
return json.load(f)[process_name]['default' if not chestnut else 'chestnut']
|
||||
|
||||
def modeld_pkl_path(chestnut: bool):
|
||||
prefix = 'big_' if chestnut else ''
|
||||
return MODELS_DIR / f'{prefix}driving_tinygrad.pkl'
|
||||
|
||||
def dump_oob(obj, f):
|
||||
with tempfile.TemporaryFile(dir=".") as tmp:
|
||||
def buffer_callback(pb: pickle.PickleBuffer):
|
||||
m = pb.raw()
|
||||
tmp.write(struct.pack('<q', m.nbytes))
|
||||
tmp.write(m)
|
||||
pb.release() # keep peak ram at ~1 buffer
|
||||
stream = io.BytesIO()
|
||||
pickle.Pickler(stream, protocol=5, buffer_callback=buffer_callback).dump(obj)
|
||||
opcodes = stream.getvalue()
|
||||
f.write(struct.pack('<q', len(opcodes)))
|
||||
f.write(opcodes)
|
||||
tmp.seek(0)
|
||||
shutil.copyfileobj(tmp, f)
|
||||
|
||||
def load_oob(f):
|
||||
opcodes = f.read(struct.unpack('<q', f.read(8))[0])
|
||||
def buffers():
|
||||
while (h := f.read(8)):
|
||||
pb = pickle.PickleBuffer(bytearray(struct.unpack('<q', h)[0]))
|
||||
if f.readinto(pb) != pb.raw().nbytes:
|
||||
raise EOFError("incomplete model buffer")
|
||||
f.readinto(pb)
|
||||
yield pb
|
||||
return pickle.load(io.BytesIO(opcodes), buffers=buffers())
|
||||
|
||||
@@ -28,13 +50,11 @@ def chestnut_present() -> bool:
|
||||
try:
|
||||
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
||||
product = (d / "product").read_text().strip()
|
||||
if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
|
||||
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN":
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def chestnut_compiled() -> bool:
|
||||
path = modeld_pkl_path(chestnut=True)
|
||||
return (path.is_file() or Path(get_manifest_path(path)).is_file()) and all(
|
||||
(MODELS_DIR / f'big_driving_warp_{size}_tinygrad.pkl').is_file() for size in ('1344x760', '1928x1208'))
|
||||
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections.abc import Callable
|
||||
import base64
|
||||
import ctypes
|
||||
from functools import cached_property
|
||||
import os
|
||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||
from tinygrad.device import Buffer, Device
|
||||
from tinygrad.dtype import DType, dtypes
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import round_up
|
||||
from tinygrad.uop.ops import UOp
|
||||
import math
|
||||
import pickle
|
||||
from tinygrad.device import Device
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import numpy as np
|
||||
@@ -33,11 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
|
||||
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
||||
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
|
||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_present, chestnut_compiled, modeld_pkl_path, load_oob
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
@@ -74,8 +75,8 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
|
||||
shouldStop=bool(stop))
|
||||
|
||||
|
||||
class ChestnutGpuState:
|
||||
# GPU metrics require modeld's GPU context
|
||||
class ChestnutState:
|
||||
# only modeld can access chestnut
|
||||
def __init__(self, pm: PubMaster, big: bool):
|
||||
self.pm = pm
|
||||
self.big = big
|
||||
@@ -89,8 +90,8 @@ class ChestnutGpuState:
|
||||
return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100)
|
||||
|
||||
def send(self) -> None:
|
||||
msg = messaging.new_message('chestnutGpuState')
|
||||
state = msg.chestnutGpuState
|
||||
msg = messaging.new_message('chestnutState')
|
||||
state = msg.chestnutState
|
||||
self.sends += 1
|
||||
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
|
||||
try:
|
||||
@@ -116,8 +117,19 @@ class ChestnutGpuState:
|
||||
for k, v in self.metrics.items():
|
||||
setattr(state, k, v)
|
||||
|
||||
msg.valid = not self.big or (self.valid and bool(self.metrics))
|
||||
self.pm.send('chestnutGpuState', msg)
|
||||
asm_valid = False
|
||||
if "AMD" in Device._opened_devices:
|
||||
try:
|
||||
# ASM runs on USB-C power, these still read without a gpu
|
||||
asm = Device["AMD"].iface.pci_dev.usb
|
||||
state.pcieLtssm = asm.read(0xB450, 1)[0]
|
||||
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
|
||||
asm_valid = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
msg.valid = asm_valid and (not self.big or self.valid)
|
||||
self.pm.send('chestnutState', msg)
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
@@ -130,64 +142,45 @@ class FrameMeta:
|
||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||
|
||||
|
||||
def input_view(buffer: Buffer, shape: tuple[int, ...], dtype: DType, offset: int) -> Tensor:
|
||||
view = buffer.view(math.prod(shape), dtype, offset).ensure_allocated()
|
||||
return Tensor(UOp.from_buffer(view)).reshape(shape)
|
||||
|
||||
|
||||
class ModelState:
|
||||
class ModelState(ModelStateBase):
|
||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
||||
ModelStateBase.__init__(self)
|
||||
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut)
|
||||
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
||||
self.model_device = jits['input_specs']['new_img'][2]
|
||||
self.input_shapes = {name: (shape, np.dtype(dtype)) for name, (shape, dtype, _) in jits['input_specs'].items()}
|
||||
self.state_pairs = {name: f'next_{name}' for name in self.input_shapes if f'next_{name}' in jits['metadata']['output_shapes']}
|
||||
self.vision_input_names = ('img', 'big_img')
|
||||
self.output_slices = pickle.loads(base64.b64decode(jits['metadata']['metadata']['output_slices']))
|
||||
metadata = jits['metadata']
|
||||
self.input_shapes = metadata['input_shapes']
|
||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||
self.output_slices = metadata['output_slices']
|
||||
|
||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||
self.chestnut = chestnut
|
||||
|
||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||
self.frame_copy_size = stride * (y_height + uv_height)
|
||||
self.pack_inputs()
|
||||
with open(MODELS_DIR / f'{"big_" if chestnut else ""}driving_warp_{cam_w}x{cam_h}_tinygrad.pkl', 'rb') as f:
|
||||
self.run_warp = pickle.load(f)['run']
|
||||
self.run_model = jits['run']
|
||||
self.outputs = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize() for name, (shape, dtype, device) in jits['output_specs'].items()}
|
||||
for name, next_name in self.state_pairs.items():
|
||||
state = self.input_queues[name]
|
||||
self.outputs[next_name] = input_view(state._buffer(), state.shape, state.dtype, 0)
|
||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
self.full_frames: dict[str, Tensor] = {}
|
||||
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||
self.parser = Parser()
|
||||
|
||||
def pack_inputs(self) -> None:
|
||||
# Pack host inputs into one upload to reduce USB transfer overhead for the eGPU.
|
||||
self.input_queues = {name: Tensor(np.zeros(shape, dtype=dtype), device=self.model_device).realize()
|
||||
for name, (shape, dtype) in self.input_shapes.items() if name in self.state_pairs}
|
||||
shapes = {'tfm': (2, 3, 3)} | {name: shape for name, (shape, _) in self.input_shapes.items()
|
||||
if name not in self.state_pairs and name != 'new_img'}
|
||||
npy_size = sum(round_up(math.prod(shape) * 4, 128) for shape in shapes.values())
|
||||
self.packed_input = np.zeros(npy_size + 2 * self.frame_copy_size, dtype=np.uint8)
|
||||
self.input_host = Tensor(self.packed_input, device='NPY')._buffer()
|
||||
self.input_device = Tensor(self.packed_input, device=self.model_device)._buffer()
|
||||
self.npy = {}
|
||||
offset = 0
|
||||
for name, shape in shapes.items():
|
||||
self.npy[name] = np.ndarray(shape, dtype=np.float32, buffer=self.packed_input, offset=offset)
|
||||
self.input_queues[name] = input_view(self.input_device, shape, dtypes.float32, offset)
|
||||
offset += round_up(self.npy[name].nbytes, 128)
|
||||
self.frames = self.packed_input[npy_size:].reshape(2, self.frame_copy_size)
|
||||
self.warp_inputs = {'input_frame': input_view(self.input_device, self.frames.shape, dtypes.uint8, npy_size), 'M_inv': self.input_queues.pop('tfm')}
|
||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||
self.run_policy = jits['run_policy']
|
||||
self.warp = jits[(cam_w,cam_h)]
|
||||
|
||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||
return {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
return parsed_model_outputs
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
||||
for i, key in enumerate(self.vision_input_names):
|
||||
np.copyto(self.frames[i], np.frombuffer(bufs[key].data, dtype=np.uint8, count=self.frame_copy_size))
|
||||
self.npy['tfm'][i] = transforms[key]
|
||||
for key in bufs.keys():
|
||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||
yuv_size = self.frame_buf_params[key][3]
|
||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||
cache_key = (key, ptr)
|
||||
if cache_key not in self._blob_cache:
|
||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||
self.full_frames[key] = self._blob_cache[cache_key]
|
||||
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs['desire_pulse'][0] = 0
|
||||
@@ -195,30 +188,35 @@ class ModelState:
|
||||
self.prev_desire[:] = inputs['desire_pulse']
|
||||
self.npy['traffic_convention'][:] = inputs['traffic_convention']
|
||||
self.npy['action_t'][:] = inputs['action_t']
|
||||
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
||||
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
||||
|
||||
self.input_device.copy_from(self.input_host)
|
||||
self.input_queues['new_img'] = self.run_warp(**self.warp_inputs)
|
||||
self.run_model(output_buffers=self.outputs, **self.input_queues)
|
||||
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
|
||||
|
||||
outs, = self.run_policy(
|
||||
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
|
||||
)
|
||||
if after_enqueue is not None:
|
||||
after_enqueue()
|
||||
model_output = self.outputs['outputs'].numpy()[0]
|
||||
model_output = outs.numpy()[0]
|
||||
if self.chestnut and not np.all(np.isfinite(model_output)):
|
||||
raise RuntimeError("model output not finite")
|
||||
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
|
||||
self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']]
|
||||
|
||||
if SEND_RAW_PRED:
|
||||
outputs_dict['raw_pred'] = model_output.copy()
|
||||
return outputs_dict
|
||||
|
||||
def warmup(self) -> None:
|
||||
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
|
||||
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
|
||||
eye = np.eye(3, dtype=np.float32)
|
||||
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
|
||||
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
|
||||
self.packed_input[:] = 0
|
||||
for key in self.state_pairs:
|
||||
self.input_queues[key].assign(0).realize()
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
self.prev_desire[:] = 0
|
||||
self.full_frames.clear()
|
||||
self._blob_cache.clear()
|
||||
|
||||
|
||||
def main(demo=False):
|
||||
@@ -279,16 +277,17 @@ def main(demo=False):
|
||||
if model is None:
|
||||
model = small_model
|
||||
params.put_bool("ChestnutLoading", False)
|
||||
assert model is not None
|
||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||
|
||||
# messaging
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutGpuState"] if CHESTNUT else [])
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
|
||||
pm = PubMaster(pub_socks)
|
||||
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
|
||||
|
||||
publish_state = PublishState()
|
||||
params = Params()
|
||||
chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None
|
||||
chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
|
||||
|
||||
# setup filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
|
||||
@@ -315,6 +314,7 @@ def main(demo=False):
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
|
||||
DH = DesireHelper()
|
||||
RELC = RoadEdgeLaneChangeController()
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -354,6 +354,7 @@ def main(demo=False):
|
||||
is_rhd = sm["driverMonitoringState"].isRHD
|
||||
frame_id = sm["narrowRoadCameraState"].frameId
|
||||
v_ego = max(sm["carState"].vEgo, 0.)
|
||||
model.lat_delay = get_lat_delay(params, sm["lateralDelay"].lateralDelay)
|
||||
lat_delay = sm["lateralDelay"].lateralDelay + LAT_SMOOTH_SECONDS
|
||||
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
|
||||
@@ -397,7 +398,7 @@ def main(demo=False):
|
||||
mt1 = time.perf_counter()
|
||||
try:
|
||||
send_chestnut = (chestnut_state is not None and
|
||||
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0)
|
||||
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0)
|
||||
model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None)
|
||||
except Exception:
|
||||
if not params.get_bool("ChestnutActive"):
|
||||
@@ -405,6 +406,7 @@ def main(demo=False):
|
||||
# fallback to small model
|
||||
cloudlog.exception("big model failed, fall back to small")
|
||||
params.put_bool("ChestnutActive", False)
|
||||
assert small_model is not None
|
||||
model = small_model
|
||||
if chestnut_state is not None:
|
||||
chestnut_state.big = False
|
||||
@@ -433,11 +435,16 @@ def main(demo=False):
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
|
||||
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
|
||||
fill_driving_model_data(drivingdata_send, modelv2_send)
|
||||
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, extrinsics_calibration_seen)
|
||||
pm.send('modelV2', modelv2_send)
|
||||
pm.send('drivingModelData', drivingdata_send)
|
||||
pm.send('cameraOdometry', posenet_send)
|
||||
pm.send('modelDataV2SP', mdv2sp_send)
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
|
||||
size 1757355221
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76cc0a9bc3af7a318889483dcbe126337f8d338f5abcbe664a8988c9b18b6639
|
||||
size 776634338
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:65a08adc31d5c456219687d99b7bf5e44d61dae2d49ea67850e76105c7248cce
|
||||
size 60918562
|
||||
oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b
|
||||
size 60881999
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
from openpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp as can_list_to_can_capnp, can_capnp_to_list as can_capnp_to_list
|
||||
from openpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp, can_capnp_to_list
|
||||
assert can_list_to_can_capnp
|
||||
assert can_capnp_to_list
|
||||
|
||||
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
||||
ps.setUptime(health.uptime_pkt);
|
||||
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
|
||||
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
|
||||
ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
|
||||
ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
|
||||
ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
|
||||
ps.setIgnitionLine(health.ignition_line_pkt);
|
||||
ps.setIgnitionCan(health.ignition_can_pkt);
|
||||
ps.setControlsAllowed(health.controls_allowed_pkt);
|
||||
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
|
||||
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
|
||||
ps.setPandaType(hw_type);
|
||||
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
|
||||
ps.setSafetyParam(health.safety_param_pkt);
|
||||
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
|
||||
ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
|
||||
ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
|
||||
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt));
|
||||
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
|
||||
ps.setAlternativeExperience(health.alternative_experience_pkt);
|
||||
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
|
||||
ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
|
||||
ps.setInterruptLoad(health.interrupt_load_pkt);
|
||||
ps.setFanPower(health.fan_power);
|
||||
ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
|
||||
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
|
||||
ps.setSpiErrorCount(health.spi_error_count_pkt);
|
||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
||||
}
|
||||
|
||||
if (spoofing_started) {
|
||||
health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
|
||||
health.ignition_line_pkt = 1;
|
||||
}
|
||||
|
||||
bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
|
||||
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad;
|
||||
|
||||
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
|
||||
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
|
||||
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
||||
}
|
||||
|
||||
bool power_save_desired = !ignition_local;
|
||||
if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
|
||||
if (health.power_save_enabled_pkt != power_save_desired) {
|
||||
panda->set_power_saving(power_save_desired);
|
||||
}
|
||||
|
||||
@@ -342,8 +342,8 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control,
|
||||
}
|
||||
}
|
||||
|
||||
// Disable IR on input timeout or when requested offroad.
|
||||
if (nanos_since_boot() - last_cabin_camera_t > 1e9 || (!is_onroad && params.getBool("DisableDriverCameraIR"))) {
|
||||
// Disable IR on input timeout
|
||||
if (nanos_since_boot() - last_cabin_camera_t > 1e9) {
|
||||
ir_pwr = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,30 +19,6 @@
|
||||
},
|
||||
"Offroad_ChestnutBranch": {
|
||||
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
|
||||
"severity": -1
|
||||
},
|
||||
"Offroad_ChestnutNotDetected": {
|
||||
"text": "Chestnut not detected. Check USB and 12V connections.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutOverheated": {
|
||||
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutPcieUnavailable": {
|
||||
"text": "%1",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutUncompiled": {
|
||||
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutUpdateFailed": {
|
||||
"text": "Chestnut update failed. Check the USB cable.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutUsbSlow": {
|
||||
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_UnregisteredHardware": {
|
||||
|
||||
@@ -198,7 +198,6 @@ class SelfdriveD(CruiseHelper):
|
||||
loading = self.params.get_bool("ChestnutLoading")
|
||||
if self.big_model_loading and not loading:
|
||||
self.big_model_ready_t = time.monotonic()
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.bigModelReady)
|
||||
self.big_model_loading = loading
|
||||
if self.big_model_loading:
|
||||
self.events.add(EventName.bigModelLoading)
|
||||
@@ -453,12 +452,11 @@ class SelfdriveD(CruiseHelper):
|
||||
self.logged_comm_issue = None
|
||||
|
||||
if not self.CP.notCar and not big_model_settling: # localization has nothing to work with during the load
|
||||
# the defaults of a message that was never received are not a localizer failure
|
||||
if self.sm.seen['deviceMotion'] and not self.sm['deviceMotion'].posenetOK:
|
||||
if not self.sm['deviceMotion'].posenetOK:
|
||||
self.events.add(EventName.posenetInvalid)
|
||||
if self.sm.seen['deviceMotion'] and not self.sm['deviceMotion'].inputsOK:
|
||||
if not self.sm['deviceMotion'].inputsOK:
|
||||
self.events.add(EventName.locationdTemporaryError)
|
||||
if (self.sm.seen['vehicleParameters'] and not self.sm['vehicleParameters'].valid and cal_status == log.ExtrinsicsCalibration.Status.calibrated and
|
||||
if (not self.sm['vehicleParameters'].valid and cal_status == log.ExtrinsicsCalibration.Status.calibrated and
|
||||
not TESTING_CLOSET and (not SIMULATION or REPLAY)):
|
||||
self.events.add(EventName.paramsdTemporaryError)
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
sudo python3 openpilot/system/hardware/chestnut/flash.py
|
||||
|
||||
SCONSFLAGS="-j4" ./openpilot/system/manager/build.py
|
||||
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
|
||||
add_ops = []
|
||||
for _, msg in msgs:
|
||||
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
||||
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
|
||||
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]:
|
||||
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
|
||||
for meta_field in ["laneChangeState", "laneChangeState"]:
|
||||
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
|
||||
|
||||
@@ -20,13 +20,11 @@ from openpilot.tools.lib.framereader import FrameReader
|
||||
from openpilot.tools.lib.logreader import LogReader, save_log
|
||||
from openpilot.tools.lib.github_utils import GithubUtils
|
||||
|
||||
TEST_ROUTE = "98395b7c5b27882e|0000002b--2686b5a2d0"
|
||||
SEGMENT = 1
|
||||
TEST_ROUTE = "8494c69d3c710e81|000001d4--2648a9a404"
|
||||
SEGMENT = 4
|
||||
START_FRAME = 0
|
||||
END_FRAME = 60
|
||||
|
||||
CHESTNUT = "--chestnut" in sys.argv
|
||||
|
||||
SEND_EXTRA_INPUTS = bool(int(os.getenv("SEND_EXTRA_INPUTS", "0")))
|
||||
|
||||
DATA_TOKEN = os.getenv("CI_ARTIFACTS_TOKEN","")
|
||||
@@ -35,13 +33,13 @@ MODEL_REPLAY_BUCKET="model_replay_master"
|
||||
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
||||
|
||||
EXEC_TIMINGS = [
|
||||
# model, instant max, average max, chestnut average max
|
||||
("modelV2", 0.05, 0.03, 0.05),
|
||||
("driverStateV2", 0.05, 0.018, 0.018),
|
||||
# model, instant max, average max
|
||||
("modelV2", 0.05, 0.028),
|
||||
("driverStateV2", 0.05, 0.018),
|
||||
]
|
||||
|
||||
def get_log_fn(test_route, ref="master"):
|
||||
return f"{test_route}_model_{'chestnut' if CHESTNUT else 'tici'}_{ref}.zst"
|
||||
return f"{test_route}_model_tici_{ref}.zst"
|
||||
|
||||
def plot(proposed, master, title, tmp):
|
||||
proposed = list(proposed)
|
||||
@@ -171,15 +169,11 @@ def model_replay(lr, frs):
|
||||
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
||||
|
||||
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
||||
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
|
||||
if CHESTNUT:
|
||||
assert chestnut and all(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2"), "Chestnut replay must run the big model without fallback"
|
||||
|
||||
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
|
||||
rows = []
|
||||
timings_ok = True
|
||||
for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
|
||||
avg_max = chestnut_avg_max if chestnut else avg_max
|
||||
for (s, instant_max, avg_max) in EXEC_TIMINGS:
|
||||
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
|
||||
# TODO some init can happen in first iteration
|
||||
ts = ts[1:]
|
||||
@@ -289,8 +283,7 @@ if __name__ == "__main__":
|
||||
diff_short, diff_long, failed = format_diff(results, log_paths, 'master')
|
||||
|
||||
if "CI" in os.environ:
|
||||
if not CHESTNUT:
|
||||
comment_replay_report(log_msgs, cmp_log, log_msgs)
|
||||
comment_replay_report(log_msgs, cmp_log, log_msgs)
|
||||
failed = False
|
||||
print(diff_long)
|
||||
print('-------------\n'*5)
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.common.hardware.hw import Paths
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from msgq.visionipc import VisionIpcClient, VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name
|
||||
from msgq.visionipc import VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.car_helpers import get_car, interfaces
|
||||
from openpilot.common.params import Params
|
||||
@@ -210,7 +210,6 @@ class ProcessContainer:
|
||||
stride, y_height, _, yuv_size = get_nv12_info(frame_size[0], frame_size[1])
|
||||
vipc_server.create_buffers_with_sizes(meta.stream, 2, frame_size[0], frame_size[1], yuv_size, stride, stride * y_height)
|
||||
vipc_server.start_listener()
|
||||
VisionIpcClient.available_streams("camerad", block=True)
|
||||
|
||||
self.vipc_server = vipc_server
|
||||
self.cfg.vision_pubs = [meta.camera_state for meta in streams_metas if meta.camera_state in self.cfg.vision_pubs]
|
||||
|
||||
@@ -3,7 +3,7 @@ set -e
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
BASEDIR=$(realpath "$SCRIPT_DIR/../../../")
|
||||
cd "$BASEDIR"
|
||||
cd $BASEDIR
|
||||
|
||||
# tests that our build system's dependencies are configured properly,
|
||||
# needs a machine with lots of cores
|
||||
@@ -11,7 +11,7 @@ cd "$BASEDIR"
|
||||
# helpful commands:
|
||||
# scons -Q --tree=derived
|
||||
|
||||
cd "$BASEDIR/opendbc_repo/"
|
||||
cd $BASEDIR/opendbc_repo/
|
||||
scons --clean
|
||||
scons --no-cache --random
|
||||
if ! scons -q; then
|
||||
|
||||
@@ -29,7 +29,7 @@ if [ -d /data/safe_staging/ ]; then
|
||||
fi
|
||||
|
||||
CONTINUE_PATH="/data/continue.sh"
|
||||
tee "$CONTINUE_PATH" << EOF
|
||||
tee $CONTINUE_PATH << EOF
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sudo abctl --set_success
|
||||
@@ -54,18 +54,13 @@ done
|
||||
|
||||
sleep infinity
|
||||
EOF
|
||||
chmod +x "$CONTINUE_PATH"
|
||||
chmod +x $CONTINUE_PATH
|
||||
|
||||
export GIT_LFS_SKIP_SMUDGE=1
|
||||
pull_lfs() {
|
||||
if [ -n "${CHESTNUT:-}" ]
|
||||
then
|
||||
git lfs pull --exclude=''
|
||||
return
|
||||
fi
|
||||
|
||||
# Keep the precompiled big model as a pointer on devices without Chestnut.
|
||||
LFS_EXCLUDE="openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl"
|
||||
# The big driving model is not used on these devices yet. Keep its pointer in
|
||||
# the worktree, but don't download or copy the 1.8 GB LFS object.
|
||||
LFS_EXCLUDE="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"
|
||||
|
||||
git config --local lfs.fetchexclude "$LFS_EXCLUDE"
|
||||
git lfs pull --exclude="$LFS_EXCLUDE"
|
||||
@@ -86,16 +81,16 @@ pull_lfs() {
|
||||
safe_checkout() {
|
||||
# completely clean TEST_DIR
|
||||
|
||||
cd "$SOURCE_DIR"
|
||||
cd $SOURCE_DIR
|
||||
|
||||
# cleanup orphaned locks
|
||||
find .git -type f -name "*.lock" -exec rm {} +
|
||||
|
||||
git reset --hard
|
||||
git fetch --no-tags --no-recurse-submodules -j4 --verbose --depth 1 origin "$GIT_COMMIT"
|
||||
git fetch --no-tags --no-recurse-submodules -j4 --verbose --depth 1 origin $GIT_COMMIT
|
||||
find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
|
||||
git reset --hard "$GIT_COMMIT"
|
||||
git checkout "$GIT_COMMIT"
|
||||
git reset --hard $GIT_COMMIT
|
||||
git checkout $GIT_COMMIT
|
||||
git clean -xdff
|
||||
git submodule sync
|
||||
git submodule foreach --recursive "git reset --hard && git clean -xdff"
|
||||
@@ -105,22 +100,22 @@ safe_checkout() {
|
||||
pull_lfs
|
||||
|
||||
echo "git checkout done, t=$SECONDS"
|
||||
du -hs "$SOURCE_DIR" "$SOURCE_DIR/.git"
|
||||
du -hs $SOURCE_DIR $SOURCE_DIR/.git
|
||||
|
||||
rsync -a --delete "$SOURCE_DIR" "$TEST_DIR"
|
||||
rsync -a --delete $SOURCE_DIR $TEST_DIR
|
||||
}
|
||||
|
||||
unsafe_checkout() {( set -e
|
||||
# checkout directly in test dir, leave old build products
|
||||
|
||||
cd "$TEST_DIR"
|
||||
cd $TEST_DIR
|
||||
|
||||
# cleanup orphaned locks
|
||||
find .git -type f -name "*.lock" -exec rm {} +
|
||||
|
||||
git fetch --no-tags --no-recurse-submodules -j8 --verbose --depth 1 origin "$GIT_COMMIT"
|
||||
git checkout --force --no-recurse-submodules "$GIT_COMMIT"
|
||||
git reset --hard "$GIT_COMMIT"
|
||||
git fetch --no-tags --no-recurse-submodules -j8 --verbose --depth 1 origin $GIT_COMMIT
|
||||
git checkout --force --no-recurse-submodules $GIT_COMMIT
|
||||
git reset --hard $GIT_COMMIT
|
||||
git clean -dff
|
||||
git submodule sync
|
||||
git submodule foreach --recursive "git reset --hard && git clean -df"
|
||||
@@ -134,7 +129,7 @@ export GIT_PACK_THREADS=8
|
||||
|
||||
# set up environment
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
git clone https://github.com/commaai/openpilot.git "$SOURCE_DIR"
|
||||
git clone https://github.com/commaai/openpilot.git $SOURCE_DIR
|
||||
fi
|
||||
|
||||
if [ ! -z "$UNSAFE" ]; then
|
||||
@@ -151,7 +146,7 @@ else
|
||||
fi
|
||||
|
||||
# submodule package symlinks for PYTHONPATH imports on device (same as launch_chffrplus.sh)
|
||||
cd "$TEST_DIR"
|
||||
cd $TEST_DIR
|
||||
ln -sfn msgq_repo/msgq msgq
|
||||
ln -sfn opendbc_repo/opendbc opendbc
|
||||
ln -sfn rednose_repo/rednose rednose
|
||||
|
||||
@@ -20,12 +20,8 @@ from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.timeout import Timeout
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.selfdrived.events import EVENTS, ET
|
||||
from openpilot.selfdrive.test.helpers import set_params_enabled, release_only, processes_context, log_collector
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.selfdrive.test.helpers import set_params_enabled, release_only
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.common.mock import mock_messages
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from openpilot.tools.lib.log_time_series import msgs_to_time_series
|
||||
|
||||
@@ -461,43 +457,5 @@ class TestOnroad(OpenpilotTestCase):
|
||||
f"Not engageable for whole segment:\n- selfdriveState.engageable: {Counter(eng)}\n- No entry events: {no_entries}"
|
||||
|
||||
|
||||
@unittest.skipUnless(HARDWARE.get_device_type() == "mici", "requires MICI")
|
||||
class TestChestnutOnroad(OpenpilotTestCase):
|
||||
COMMA_HARDWARE_TEST = True
|
||||
|
||||
@mock_messages(['deviceMotion'])
|
||||
def test_camera_models(self, subtests):
|
||||
assert chestnut_present() and chestnut_compiled()
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes(), block=True)
|
||||
services = ['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState', 'modelV2', 'driverStateV2']
|
||||
sm = messaging.SubMaster(services)
|
||||
pm = messaging.PubMaster(['deviceState'])
|
||||
device_state = messaging.new_message('deviceState')
|
||||
device_state.deviceState.deviceType = HARDWARE.get_device_type()
|
||||
device_state_bytes = device_state.to_bytes()
|
||||
with processes_context(['camerad', 'calibrationd', 'modeld', 'dmonitoringmodeld']):
|
||||
with Timeout(60, "camera models didn't start"):
|
||||
while not all(sm.seen.values()) or not sm.valid['modelV2']:
|
||||
pm.send('deviceState', device_state_bytes)
|
||||
sm.update(1000)
|
||||
with log_collector(services) as (logs, _):
|
||||
time.sleep(TEST_DURATION)
|
||||
|
||||
msgs = {s: [m for m in logs if m.which() == s] for s in services}
|
||||
for service, messages in msgs.items():
|
||||
with subtests.test(service=service):
|
||||
expected = TEST_DURATION * SERVICE_LIST[service].frequency
|
||||
assert np.isclose(len(messages), expected, rtol=0.05, atol=2), f"{service}: expected {expected}, got {len(messages)}"
|
||||
assert all(m.valid for m in messages)
|
||||
frame_ids = [getattr(m, service).frameId for m in messages]
|
||||
assert np.all(np.diff(frame_ids) > 0), f"{service}: repeated or reordered frames"
|
||||
|
||||
camera_frames = {m.narrowRoadCameraState.frameId for m in msgs['narrowRoadCameraState']}
|
||||
model_frames = {m.modelV2.frameId for m in msgs['modelV2']}
|
||||
assert len(camera_frames & model_frames) >= TEST_DURATION * SERVICE_LIST['modelV2'].frequency * 0.9
|
||||
assert all(m.modelV2.big for m in msgs['modelV2']), "Chestnut fell back to the small model"
|
||||
assert all(np.isfinite(m.modelV2.position.x).all() for m in msgs['modelV2'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -5,8 +5,6 @@ import time
|
||||
import unittest
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from panda import Panda
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.utils import tabulate
|
||||
|
||||
@@ -16,14 +14,11 @@ from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.common.mock import mock_messages
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.hardware.comma.power_monitor import get_power
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.system.manager.manager import manager_cleanup
|
||||
|
||||
SAMPLE_TIME = 2 # seconds to sample power
|
||||
MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples
|
||||
MICI = HARDWARE.get_device_type() == "mici"
|
||||
CHESTNUT = chestnut_present()
|
||||
|
||||
@dataclass
|
||||
class Proc:
|
||||
@@ -38,10 +33,9 @@ class Proc:
|
||||
return '+'.join(self.procs)
|
||||
|
||||
|
||||
# MICI readings exclude the separately powered Chestnut GPU.
|
||||
PROCS = [
|
||||
Proc(['camerad'], 0.85 if MICI else 1.65, atol=0.4, msgs=['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']),
|
||||
Proc(['modeld'], 0.45 if MICI and CHESTNUT else 1.5, atol=0.2, msgs=['modelV2']),
|
||||
Proc(['camerad'], 1.65, atol=0.4, msgs=['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']),
|
||||
Proc(['modeld'], 1.5, atol=0.2, msgs=['modelV2']),
|
||||
Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']),
|
||||
Proc(['encoderd'], 0.23, msgs=[]),
|
||||
]
|
||||
@@ -52,13 +46,6 @@ class TestPowerDraw(OpenpilotTestCase):
|
||||
|
||||
def setup_method(self):
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes(), block=True)
|
||||
self.panda = None
|
||||
if MICI:
|
||||
HARDWARE.reset_internal_panda()
|
||||
self.addCleanup(HARDWARE.reset_internal_panda)
|
||||
Panda.wait_for_panda(None, 30)
|
||||
self.panda = Panda(cli=False)
|
||||
self.addCleanup(self.panda.close)
|
||||
|
||||
def teardown_method(self):
|
||||
manager_cleanup()
|
||||
@@ -91,7 +78,7 @@ class TestPowerDraw(OpenpilotTestCase):
|
||||
start_time = time.monotonic()
|
||||
|
||||
while (time.monotonic() - start_time) < MAX_WARMUP_TIME:
|
||||
power = get_power(1, self.panda)
|
||||
power = get_power(1)
|
||||
iteration_msg_counts = {}
|
||||
for msg,sock in socks.items():
|
||||
iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock))
|
||||
@@ -110,7 +97,7 @@ class TestPowerDraw(OpenpilotTestCase):
|
||||
|
||||
@mock_messages(['deviceMotion'])
|
||||
def test_camera_procs(self, subtests):
|
||||
baseline = get_power(panda=self.panda)
|
||||
baseline = get_power()
|
||||
|
||||
prev = baseline
|
||||
used = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
|
||||
self._last_input_time = time.monotonic()
|
||||
self._was_active = False
|
||||
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
|
||||
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
|
||||
|
||||
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
|
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -178,7 +178,7 @@ class HomeLayout(Widget):
|
||||
|
||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||
version_text_width, self.header_rect.height)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
|
||||
@@ -5,7 +5,7 @@ from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
@@ -115,9 +115,9 @@ class TermsPage(Widget):
|
||||
self._on_accept = on_accept
|
||||
self._on_decline = on_decline
|
||||
|
||||
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
|
||||
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
click_callback=self._on_uninstall_clicked)
|
||||
|
||||
@@ -199,9 +199,6 @@ class SoftwareLayout(Widget):
|
||||
selection = self._branch_dialog.selection
|
||||
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
|
||||
self._branch_btn.action_item.set_value(selection)
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||
self._branch_dialog = None
|
||||
|
||||
|
||||
@@ -101,10 +101,6 @@ class PrimeState:
|
||||
with self._lock:
|
||||
return bool(self.prime_type > PrimeType.NONE)
|
||||
|
||||
def is_full_prime(self) -> bool:
|
||||
with self._lock:
|
||||
return self.prime_type > PrimeType.NONE and self.prime_type != PrimeType.LITE
|
||||
|
||||
def is_paired(self) -> bool:
|
||||
with self._lock:
|
||||
return self.prime_type > PrimeType.UNPAIRED
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import math
|
||||
import time
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -11,7 +8,7 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.common.version import RELEASE_BRANCHES
|
||||
|
||||
@@ -41,13 +38,16 @@ class AlertsPill(Widget):
|
||||
self.set_rect(rl.Rectangle(0, 0, 104, 52))
|
||||
|
||||
self._pill_bg_txt = gui_app.texture("icons_mici/alerts_pill.png", 104, 52)
|
||||
self._icon_red = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", 36, 36)
|
||||
self._icon_orange = gui_app.texture("icons_mici/offroad_alerts/orange_warning.png", 36, 36)
|
||||
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", 36, 36)
|
||||
self._alert_count_callback: Callable[[], int] | None = None
|
||||
self._alert_icon_callback: Callable[[], rl.Texture | None] | None = None
|
||||
self._max_severity_callback: Callable[[], int | None] | None = None
|
||||
|
||||
def set_alert_count_callback(self, callback: Callable[[], int] | None,
|
||||
icon_callback: Callable[[], rl.Texture | None] | None = None):
|
||||
severity_callback: Callable[[], int | None] | None = None):
|
||||
self._alert_count_callback = callback
|
||||
self._alert_icon_callback = icon_callback
|
||||
self._max_severity_callback = severity_callback
|
||||
|
||||
def _render(self, _):
|
||||
alert_count = self._alert_count_callback() if self._alert_count_callback else 0
|
||||
@@ -55,37 +55,42 @@ class AlertsPill(Widget):
|
||||
pill_w, pill_h = self._pill_bg_txt.width, self._pill_bg_txt.height
|
||||
rl.draw_texture_ex(self._pill_bg_txt, rl.Vector2(self.rect.x, self.rect.y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
warning_txt = self._alert_icon_callback() if self._alert_icon_callback else None
|
||||
if warning_txt is not None:
|
||||
scale = 36 / max(warning_txt.width, warning_txt.height)
|
||||
warn_x = self.rect.x + self.ICON_OFFSET
|
||||
warn_y = self.rect.y + (pill_h - warning_txt.height * scale) / 2
|
||||
rl.draw_texture_ex(warning_txt, rl.Vector2(warn_x, warn_y), 0.0, scale, rl.WHITE)
|
||||
severity = self._max_severity_callback() if self._max_severity_callback else None
|
||||
if severity == -1:
|
||||
warning_txt = self._icon_green
|
||||
elif severity is not None and severity > 0:
|
||||
warning_txt = self._icon_red
|
||||
else:
|
||||
warning_txt = self._icon_orange
|
||||
|
||||
warn_x = self.rect.x + self.ICON_OFFSET
|
||||
warn_y = self.rect.y + (pill_h - warning_txt.height) / 2
|
||||
rl.draw_texture_ex(warning_txt, rl.Vector2(warn_x, warn_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
||||
gui_label(count_rect, str(alert_count), font_size=36,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
|
||||
class NetworkIcon(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 60, 47)) # max size of all icons
|
||||
self.set_rect(rl.Rectangle(0, 0, 54, 44)) # max size of all icons
|
||||
self._net_type = NetworkType.none
|
||||
self._net_strength = 0
|
||||
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 54, 47)
|
||||
self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 54, 40)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 54, 40)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 54, 40)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 54, 40)
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 50, 44)
|
||||
self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 50, 37)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 37)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 37)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 37)
|
||||
|
||||
self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 60, 40)
|
||||
self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 60, 40)
|
||||
self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 60, 40)
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 60, 40)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 60, 40)
|
||||
self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 54, 36)
|
||||
self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 54, 36)
|
||||
self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 54, 36)
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 54, 36)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 54, 36)
|
||||
|
||||
def _update_state(self):
|
||||
device_state = ui_state.sm['deviceState']
|
||||
@@ -134,9 +139,7 @@ class MiciHomeLayout(Widget):
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
|
||||
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (54, 40))
|
||||
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
||||
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
|
||||
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
|
||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
||||
@@ -147,15 +150,13 @@ class MiciHomeLayout(Widget):
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
self._experimental_icon,
|
||||
self._usb_icon,
|
||||
self._chestnut_icon,
|
||||
self._chestnut_loading_icon,
|
||||
self._chestnut_failed_icon,
|
||||
self._body_icon,
|
||||
self._mic_icon,
|
||||
], spacing=18)
|
||||
|
||||
self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
@@ -181,11 +182,11 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None,
|
||||
alert_count_callback: Callable[[], int] | None = None,
|
||||
alert_icon_callback: Callable[[], rl.Texture | None] | None = None):
|
||||
max_severity_callback: Callable[[], int | None] | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
self._on_alerts_click = on_alerts
|
||||
self._alert_count_callback = alert_count_callback
|
||||
self._alerts_pill.set_alert_count_callback(alert_count_callback, alert_icon_callback)
|
||||
self._alerts_pill.set_alert_count_callback(alert_count_callback, max_severity_callback)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if not self._did_long_press:
|
||||
@@ -246,20 +247,12 @@ class MiciHomeLayout(Widget):
|
||||
self._version_commit_label.render()
|
||||
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
usb_connected = ui_state.usb_connected
|
||||
usb_unknown = ui_state.usb_unknown
|
||||
chestnut_state = ui_state.chestnut_state
|
||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||
if gui_app.sunnypilot_ui():
|
||||
self._set_chestnut_visibility()
|
||||
else:
|
||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
||||
self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
|
||||
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
|
||||
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
|
||||
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
|
||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE))
|
||||
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class MiciMainLayout(Scroller):
|
||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||
on_alerts=lambda: self._scroll_to(self._alerts_layout),
|
||||
alert_count_callback=self._alerts_layout.active_alerts,
|
||||
alert_icon_callback=self._alerts_layout.highest_severity_icon,
|
||||
max_severity_callback=self._alerts_layout.max_severity,
|
||||
)
|
||||
for layout in (self._car_onroad_layout, self._body_onroad_layout):
|
||||
layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pyray as rl
|
||||
import re
|
||||
import threading
|
||||
@@ -13,7 +11,7 @@ from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
REFRESH_INTERVAL = 5.0 # seconds
|
||||
@@ -31,7 +29,6 @@ class AlertData:
|
||||
text: str
|
||||
severity: int
|
||||
visible: bool = False
|
||||
icon: str | None = None
|
||||
|
||||
|
||||
class AlertItem(Widget):
|
||||
@@ -59,28 +56,18 @@ class AlertItem(Widget):
|
||||
self._bg_big = gui_app.texture("icons_mici/offroad_alerts/big_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_BIG)
|
||||
self._bg_big_pressed = gui_app.texture("icons_mici/offroad_alerts/big_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_BIG)
|
||||
|
||||
# Load alert icons
|
||||
# Load warning icons
|
||||
self._icon_orange = gui_app.texture("icons_mici/offroad_alerts/orange_warning.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._icon_red = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._custom_icon = gui_app.texture(alert_data.icon, self.ICON_SIZE, self.ICON_SIZE) if alert_data.icon else None
|
||||
|
||||
if self._custom_icon is not None:
|
||||
self._icon = self._custom_icon
|
||||
elif alert_data.severity == -1:
|
||||
self._icon = self._icon_green
|
||||
elif alert_data.severity > 0:
|
||||
self._icon = self._icon_red
|
||||
else:
|
||||
self._icon = self._icon_orange
|
||||
|
||||
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
||||
alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95)
|
||||
|
||||
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
||||
alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95)
|
||||
|
||||
self._title_text = ""
|
||||
self._body_text = ""
|
||||
@@ -88,10 +75,6 @@ class AlertItem(Widget):
|
||||
|
||||
self._update_content()
|
||||
|
||||
@property
|
||||
def icon(self) -> rl.Texture:
|
||||
return self._icon
|
||||
|
||||
def _split_text(self, text: str) -> tuple[str, str]:
|
||||
"""Split text into title (first sentence) and body (remaining text)."""
|
||||
# Find the end of the first sentence (period, exclamation, or question mark followed by space or end)
|
||||
@@ -193,9 +176,16 @@ class AlertItem(Widget):
|
||||
self._body_label.render(body_rect)
|
||||
|
||||
# Draw warning icon on the right side
|
||||
# Use green icon for update alerts (severity = -1), red for high severity, orange for low severity
|
||||
if self.alert_data.severity == -1:
|
||||
icon_texture = self._icon_green
|
||||
elif self.alert_data.severity > 0:
|
||||
icon_texture = self._icon_red
|
||||
else:
|
||||
icon_texture = self._icon_orange
|
||||
icon_x = self._rect.x + self.ALERT_WIDTH - self.ALERT_PADDING - self.ICON_SIZE
|
||||
icon_y = self._rect.y + self.ALERT_PADDING
|
||||
rl.draw_texture_ex(self._icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE)
|
||||
rl.draw_texture_ex(icon_texture, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class MiciOffroadAlerts(Scroller):
|
||||
@@ -210,8 +200,8 @@ class MiciOffroadAlerts(Scroller):
|
||||
|
||||
# Create empty state label
|
||||
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
# Build initial alert list
|
||||
self._build_alerts()
|
||||
@@ -224,10 +214,8 @@ class MiciOffroadAlerts(Scroller):
|
||||
def active_alerts(self) -> int:
|
||||
return sum(alert.visible for alert in self.sorted_alerts)
|
||||
|
||||
def highest_severity_icon(self) -> rl.Texture | None:
|
||||
item = max((item for item in self.alert_items if item.alert_data.visible),
|
||||
key=lambda item: (item.alert_data.severity, bool(item.alert_data.icon)), default=None)
|
||||
return item.icon if item is not None else None
|
||||
def max_severity(self) -> int | None:
|
||||
return max((alert.severity for alert in self.sorted_alerts if alert.visible), default=None)
|
||||
|
||||
def scrolling(self):
|
||||
return self._scroller.scroll_panel.is_touch_valid()
|
||||
@@ -247,7 +235,7 @@ class MiciOffroadAlerts(Scroller):
|
||||
# Add regular alerts sorted by severity
|
||||
for key, config in sorted(OFFROAD_ALERTS.items(), key=lambda x: x[1].get("severity", 0), reverse=True):
|
||||
severity = config.get("severity", 0)
|
||||
alert_data = AlertData(key=key, text="", severity=severity, icon=config.get("icon"))
|
||||
alert_data = AlertData(key=key, text="", severity=severity)
|
||||
self.sorted_alerts.append(alert_data)
|
||||
|
||||
# Create alert item widget
|
||||
|
||||
@@ -4,7 +4,7 @@ import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.qrcode import make_texture
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
||||
@@ -35,7 +35,7 @@ class DriverCameraSetupDialog(BaseCabinCameraDialog):
|
||||
|
||||
if not self._camera_view.frame:
|
||||
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
||||
alignment=TextAlignment.CENTER)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
rl.end_scissor_mode()
|
||||
return
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ class AlphaLongConfirmPage(NavScroller):
|
||||
GreyBigButton("", "WARNING: alpha longitudinal control may disable Automatic Emergency Braking (AEB)"),
|
||||
GreyBigButton("", "On this car, openpilot defaults to the stock system's built-in ACC."),
|
||||
GreyBigButton("", "Enabling this will switch to openpilot longitudinal control."),
|
||||
GreyBigButton("", "Using Experimental mode is recommended with openpilot longitudinal control alpha."),
|
||||
GreyBigButton("", "Changing this setting will restart openpilot if the car is powered on."),
|
||||
accept,
|
||||
])
|
||||
|
||||
@@ -61,31 +63,28 @@ class DeveloperLayoutMici(NavScroller):
|
||||
|
||||
txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64)
|
||||
github_username = ui_state.params.get("GithubUsername") or ""
|
||||
self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh,
|
||||
description="Grant SSH access to all public keys in your GitHub settings. Only enter your own username.")
|
||||
self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh)
|
||||
self._ssh_keys_btn.set_click_callback(ssh_keys_callback)
|
||||
|
||||
# adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address
|
||||
# ******** Main Scroller ********
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12),
|
||||
description="Use Android Debug Bridge (ADB) over USB or the network.", title="enable ADB")
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12),
|
||||
description="Access the device remotely using your SSH keys.", title="enable SSH")
|
||||
self._joystick_toggle = BigToggle("joystick debug\nmode", initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||
toggle_callback=self._on_joystick_debug_mode, description="Control the car with a joystick for debugging.")
|
||||
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode", initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||
toggle_callback=self._on_long_maneuver_mode,
|
||||
description="Run longitudinal maneuvers for testing gas and brake control.")
|
||||
self._lat_maneuver_toggle = BigToggle("lateral maneuver mode", initial_state=ui_state.params.get_bool("LateralManeuverMode"),
|
||||
toggle_callback=self._on_lat_maneuver_mode,
|
||||
description="Run lateral maneuvers for testing steering control.")
|
||||
self._alpha_long_toggle = BigToggle("alpha longitudinal", initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
||||
toggle_callback=self._on_alpha_long_enabled,
|
||||
description="Use alpha openpilot longitudinal control instead of stock ACC. This may disable Automatic Emergency " +
|
||||
"Braking (AEB).")
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
||||
self._joystick_toggle = BigToggle("joystick debug mode",
|
||||
initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||
toggle_callback=self._on_joystick_debug_mode)
|
||||
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode",
|
||||
initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||
toggle_callback=self._on_long_maneuver_mode)
|
||||
self._lat_maneuver_toggle = BigToggle("lateral maneuver mode",
|
||||
initial_state=ui_state.params.get_bool("LateralManeuverMode"),
|
||||
toggle_callback=self._on_lat_maneuver_mode)
|
||||
self._alpha_long_toggle = BigToggle("alpha longitudinal",
|
||||
initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
||||
toggle_callback=self._on_alpha_long_enabled)
|
||||
self._debug_mode_toggle = BigParamControl("ui debug mode", "ShowDebugInfo",
|
||||
toggle_callback=lambda checked: (gui_app.set_show_touches(checked), gui_app.set_show_fps(checked)),
|
||||
description="Show touch locations and the UI frame rate.")
|
||||
toggle_callback=lambda checked: (gui_app.set_show_touches(checked),
|
||||
gui_app.set_show_fps(checked)))
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._adb_toggle,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from typing import Union
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
@@ -78,16 +77,15 @@ def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.T
|
||||
|
||||
class EngagedConfirmationCircleButton(BigCircleButton):
|
||||
def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True,
|
||||
red: bool = False, icon_offset: tuple[int, int] = (0, 0), *, description: str = ""):
|
||||
super().__init__(icon, red, icon_offset, description=description, title=title)
|
||||
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, red, icon_offset)
|
||||
self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
|
||||
class EngagedConfirmationButton(BigButton):
|
||||
def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False, *, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(text, "", icon, description=description, description_icon=description_icon)
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
super().__init__(text, "", icon)
|
||||
self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
|
||||
@@ -179,9 +177,7 @@ class DeviceLayoutMici(NavScroller):
|
||||
params.put_bool("OnroadCycleRequested", True, block=True)
|
||||
|
||||
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
|
||||
reset_calibration_callback,
|
||||
description="Mount the device within 4° left or right and 5° up or 9° down. openpilot calibrates " +
|
||||
"continuously; resetting is rarely needed. Resetting clears learned calibration.")
|
||||
reset_calibration_callback)
|
||||
|
||||
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
|
||||
reboot_callback, exit_on_confirm=False)
|
||||
@@ -193,8 +189,7 @@ class DeviceLayoutMici(NavScroller):
|
||||
regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
regulatory_btn.set_click_callback(self._on_regulatory)
|
||||
|
||||
cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
|
||||
description="Preview the cabin camera to check driver monitoring visibility. The vehicle must be off.")
|
||||
cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64))
|
||||
cabin_cam_btn.set_click_callback(lambda: gui_app.push_widget(CabinCameraDialog()))
|
||||
cabin_cam_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
|
||||
@@ -1,60 +1,13 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.cereal import log
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.cellular_manager import CellularManager
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid
|
||||
|
||||
NetworkStrength = log.DeviceState.NetworkStrength
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
|
||||
class EsimNetworkButton(BigButton):
|
||||
def __init__(self, cellular_manager: CellularManager, *, description: str = ""):
|
||||
self._cellular_manager = cellular_manager
|
||||
self._cell_icons = {
|
||||
NetworkStrength.unknown: gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 64, 47),
|
||||
NetworkStrength.poor: gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 64, 47),
|
||||
NetworkStrength.moderate: gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 64, 47),
|
||||
NetworkStrength.good: gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 64, 47),
|
||||
NetworkStrength.great: gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 64, 47),
|
||||
}
|
||||
super().__init__("esim", "loading...", self._cell_icons[NetworkStrength.unknown], scroll=True, description=description)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self.set_enabled(self._cellular_manager.is_euicc is not False)
|
||||
text, value, icon = self._compute_state()
|
||||
self.set_text(text)
|
||||
self.set_value(value)
|
||||
self.set_icon(icon)
|
||||
|
||||
def _compute_state(self):
|
||||
cm = self._cellular_manager
|
||||
none_icon = self._cell_icons[NetworkStrength.unknown]
|
||||
ip = cm.modem_state.get("ip_address") or "connecting..."
|
||||
if cm.is_euicc is False:
|
||||
iccid = cm.modem_state.get("iccid") or ""
|
||||
if not iccid:
|
||||
return "sim", "no sim", none_icon
|
||||
return f"sim (...{iccid[-4:]})", ip, self._cell_icon()
|
||||
|
||||
active = cm.active_profile
|
||||
if active is None:
|
||||
return "esim", "loading...", none_icon
|
||||
return active.display_name, ip, self._cell_icon()
|
||||
|
||||
def _cell_icon(self):
|
||||
# read directly from HARDWARE so it reflects modem state even when wifi is the active connection
|
||||
strength = HARDWARE.get_network_strength(NetworkType.cell4G)
|
||||
return self._cell_icons.get(strength, self._cell_icons[NetworkStrength.unknown])
|
||||
|
||||
|
||||
class WifiNetworkButton(BigButton):
|
||||
def __init__(self, wifi_manager: WifiManager, *, description: str = ""):
|
||||
def __init__(self, wifi_manager: WifiManager):
|
||||
self._wifi_manager = wifi_manager
|
||||
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 28, 36)
|
||||
self._draw_lock = False
|
||||
@@ -64,7 +17,7 @@ class WifiNetworkButton(BigButton):
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
|
||||
|
||||
super().__init__("wi-fi", "not connected", self._wifi_slash_txt, scroll=True, description=description)
|
||||
super().__init__("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
@@ -1,412 +0,0 @@
|
||||
import threading
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.cereal import log
|
||||
from openpilot.cereal.visionipc import VisionStreamType
|
||||
|
||||
from openpilot.common import qrcode
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraView
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog, BigConfirmationDialog
|
||||
from openpilot.common.esim.base import Profile
|
||||
from openpilot.common.esim.lpa import parse_lpa_activation_code
|
||||
from openpilot.system.ui.lib.application import DEFAULT_TEXT_COLOR, FontWeight, MousePos, TextAlignment, gui_app
|
||||
from openpilot.system.ui.lib.cellular_manager import CellularManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
|
||||
|
||||
|
||||
class ProfileActionButton(Widget):
|
||||
SIZE = 68
|
||||
MARGIN = 10
|
||||
HORIZONTAL_MARGIN = 4
|
||||
|
||||
def __init__(self, callback: Callable, delete: bool = False):
|
||||
super().__init__()
|
||||
self.set_click_callback(callback)
|
||||
self._delete = delete
|
||||
self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 25, 30) if delete else None
|
||||
|
||||
self._bg_txt = gui_app.texture("icons_mici/buttons/button_circle.png", self.SIZE, self.SIZE)
|
||||
self._bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_pressed.png", self.SIZE, self.SIZE)
|
||||
self.set_rect(rl.Rectangle(0, 0, self.SIZE + self.HORIZONTAL_MARGIN * 2, self.SIZE + self.MARGIN * 2))
|
||||
|
||||
def _render(self, _):
|
||||
bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt
|
||||
rl.draw_texture_ex(bg_txt, (self._rect.x + (self._rect.width - self._bg_txt.width) / 2,
|
||||
self._rect.y + (self._rect.height - self._bg_txt.height) / 2), 0, 1.0, rl.WHITE)
|
||||
color = rl.Color(255, 105, 115, 255) if self._delete else DEFAULT_TEXT_COLOR
|
||||
if not self.enabled:
|
||||
color = rl.Color(color.r, color.g, color.b, 90)
|
||||
if self._trash_txt:
|
||||
rl.draw_texture_ex(self._trash_txt, (self._rect.x + (self._rect.width - self._trash_txt.width) / 2,
|
||||
self._rect.y + (self._rect.height - self._trash_txt.height) / 2), 0, 1.0, color)
|
||||
else:
|
||||
gui_label(self._rect, "Aa", 30, color=color, alignment=TextAlignment.CENTER)
|
||||
|
||||
|
||||
class QRScannerDialog(NavWidget):
|
||||
SCAN_INTERVAL_S = 0.25
|
||||
INVALID_CODE_DURATION_S = 1.0
|
||||
|
||||
def __init__(self, on_qr_detected: Callable[[str], None]):
|
||||
super().__init__()
|
||||
self._on_qr_detected = on_qr_detected
|
||||
self._camera_view = CabinCameraView("camerad", VisionStreamType.VISION_STREAM_CABIN)
|
||||
self._detected = False
|
||||
self._last_scan_time = 0.0
|
||||
self._invalid_code_until = 0.0
|
||||
self._scan_thread: threading.Thread | None = None
|
||||
self._scan_result: str | None = None
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
ui_state.params.put_bool("DisableDriverCameraIR", True)
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
ui_state.params.put_bool("DisableDriverCameraIR", False)
|
||||
|
||||
def __del__(self):
|
||||
self._camera_view.close()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
now = rl.get_time()
|
||||
if self._detected or not self._camera_view.frame or now < self._invalid_code_until:
|
||||
return
|
||||
|
||||
if self._scan_thread is not None:
|
||||
if self._scan_thread.is_alive():
|
||||
return
|
||||
self._scan_thread = None
|
||||
data = self._scan_result
|
||||
if data is not None:
|
||||
try:
|
||||
parse_lpa_activation_code(data)
|
||||
except ValueError:
|
||||
self._invalid_code_until = now + self.INVALID_CODE_DURATION_S
|
||||
self._last_scan_time = self._invalid_code_until
|
||||
else:
|
||||
self._detected = True
|
||||
self.dismiss(lambda: self._on_qr_detected(data))
|
||||
return
|
||||
|
||||
if now - self._last_scan_time < self.SCAN_INTERVAL_S:
|
||||
return
|
||||
self._last_scan_time = now
|
||||
|
||||
frame = self._camera_view.frame
|
||||
y = np.frombuffer(frame.data, dtype=np.uint8, count=frame.height * frame.stride).reshape(frame.height, frame.stride)
|
||||
gray = y[:, :frame.width].copy() # the vision buffer is recycled under the scan thread
|
||||
self._scan_thread = threading.Thread(target=self._scan, args=(gray,), daemon=True)
|
||||
self._scan_thread.start()
|
||||
|
||||
def _scan(self, gray: np.ndarray):
|
||||
self._scan_result = qrcode.decode(gray)
|
||||
|
||||
def _render(self, rect):
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._camera_view._render(rect)
|
||||
|
||||
if not self._camera_view.frame:
|
||||
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
||||
alignment=TextAlignment.CENTER)
|
||||
else:
|
||||
label_y = rect.y + rect.height * 3 / 4
|
||||
label_rect = rl.Rectangle(rect.x, label_y + (rect.height - label_y) / 2 - 20, rect.width, 40)
|
||||
text = "not an LPA code" if rl.get_time() < self._invalid_code_until else "hold QR code to camera"
|
||||
gui_label(label_rect, text, font_size=32, font_weight=FontWeight.MEDIUM,
|
||||
alignment=TextAlignment.CENTER,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
|
||||
class InstallingProfileDialog(BigDialog):
|
||||
DOT_STEP = 0.6
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("installing profile", "please wait...")
|
||||
self._show_time = 0.0
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._nav_bar._alpha = 0.0
|
||||
self._show_time = rl.get_time()
|
||||
|
||||
def _back_enabled(self) -> bool:
|
||||
return False
|
||||
|
||||
def _render(self, _):
|
||||
t = (rl.get_time() - self._show_time) % (self.DOT_STEP * 2)
|
||||
dots = "." * min(int(t / (self.DOT_STEP / 4)), 3)
|
||||
self._card.set_value(f"please wait{dots}")
|
||||
super()._render(_)
|
||||
|
||||
|
||||
class EsimProfileButton(BigButton):
|
||||
SUB_LABEL_DISABLED = rl.Color(255, 255, 255, int(255 * 0.585))
|
||||
CHECK_ICON_COLOR = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
LABEL_PADDING = 98
|
||||
LABEL_WIDTH = 402 - 98 - 28
|
||||
SUB_LABEL_WIDTH = 402 - BigButton.LABEL_HORIZONTAL_PADDING * 2
|
||||
|
||||
def __init__(self, profile: Profile, cellular_manager: CellularManager, profiles_enabled: Callable[[], bool]):
|
||||
self._cellular_manager = cellular_manager
|
||||
self._profiles_enabled = profiles_enabled
|
||||
super().__init__(profile.display_name, scroll=True)
|
||||
|
||||
self._profile = profile
|
||||
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 48, 36)
|
||||
self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 48, 36)
|
||||
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
|
||||
self._comma_txt = gui_app.texture("icons_mici/settings/comma_icon.png", 36, 36) if profile.is_comma else None
|
||||
|
||||
self._delete_btn = ProfileActionButton(self._on_delete, delete=True)
|
||||
self._rename_btn = ProfileActionButton(self._on_rename) if not profile.is_comma else None
|
||||
self._delete_btn.set_enabled(lambda: not self._locked and not self._cellular_manager.busy and self._show_delete_btn)
|
||||
if self._rename_btn:
|
||||
self._rename_btn.set_enabled(lambda: not self._locked and not self._cellular_manager.busy)
|
||||
self.set_enabled(lambda: not self._profile.enabled and self._profiles_enabled() and not self._cellular_manager.busy)
|
||||
self.update_profile(profile)
|
||||
|
||||
@property
|
||||
def profile(self) -> Profile:
|
||||
return self._profile
|
||||
|
||||
def update_profile(self, profile: Profile):
|
||||
self._profile = profile
|
||||
active = profile.enabled
|
||||
self.set_text(profile.display_name)
|
||||
self.set_value("active" if active else "switch")
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._sub_label.set_color(DEFAULT_TEXT_COLOR if self.enabled else self.SUB_LABEL_DISABLED)
|
||||
self._sub_label.set_font_weight(FontWeight.SEMI_BOLD if self.enabled else FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _locked(self) -> bool:
|
||||
return not self._profile.is_comma and not self._profiles_enabled()
|
||||
|
||||
@property
|
||||
def _show_delete_btn(self) -> bool:
|
||||
return not self._profile.enabled and not self._profile.is_comma
|
||||
|
||||
def _on_rename(self):
|
||||
current = self._profile.nickname or ""
|
||||
dlg = BigInputDialog("nickname", default_text=current, confirm_callback=self._on_nickname_entered,
|
||||
text_validator=lambda text: bool(text.strip()))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _on_delete(self):
|
||||
icon = gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64)
|
||||
gui_app.push_widget(BigConfirmationDialog("slide to delete", icon, self._delete_profile, red=True))
|
||||
|
||||
def _delete_profile(self):
|
||||
if not self._locked and not self._cellular_manager.busy and self._show_delete_btn:
|
||||
if ui_state.sm["deviceState"].networkType == log.DeviceState.NetworkType.none:
|
||||
gui_app.push_widget(BigDialog("", tr("Ensure you're connected to the internet and try again.")))
|
||||
return
|
||||
self._cellular_manager.delete_profile(self._profile.iccid)
|
||||
|
||||
def _on_nickname_entered(self, nickname: str):
|
||||
if not self._locked and not self._cellular_manager.busy:
|
||||
self._cellular_manager.nickname_profile(self._profile.iccid, nickname.strip())
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._show_delete_btn and rl.check_collision_point_rec(mouse_pos, self._delete_btn.rect):
|
||||
return
|
||||
if self._rename_btn is not None and rl.check_collision_point_rec(mouse_pos, self._rename_btn.rect):
|
||||
return
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 48
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
self._label.set_color(self.SUB_LABEL_DISABLED if self._locked else LABEL_COLOR)
|
||||
label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + self.LABEL_VERTICAL_PADDING,
|
||||
self.LABEL_WIDTH, self._rect.height - self.LABEL_VERTICAL_PADDING * 2)
|
||||
self._label.render(label_rect)
|
||||
|
||||
active = self._profile.enabled
|
||||
|
||||
if self.value:
|
||||
sub_label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING
|
||||
label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING
|
||||
action_w = self._rename_btn.rect.width if self._rename_btn is not None else 0
|
||||
action_w += self._delete_btn.rect.width if self._show_delete_btn else 0
|
||||
sub_label_w = self.SUB_LABEL_WIDTH - action_w
|
||||
sub_label_height = self._sub_label.get_content_height(sub_label_w)
|
||||
|
||||
if active:
|
||||
check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2)
|
||||
rl.draw_texture_ex(self._check_txt, rl.Vector2(sub_label_x, check_y), 0.0, 1.0, self.CHECK_ICON_COLOR)
|
||||
sub_label_x += self._check_txt.width + 14
|
||||
|
||||
sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height)
|
||||
self._sub_label.render(sub_label_rect)
|
||||
|
||||
if self._comma_txt:
|
||||
rl.draw_texture_ex(self._comma_txt, (self._rect.x + 36, btn_y + 38), 0.0, 1.0, rl.WHITE)
|
||||
else:
|
||||
cell_icon = self._cell_full_txt if active else self._cell_none_txt
|
||||
rl.draw_texture_ex(cell_icon, (self._rect.x + 30, btn_y + 38), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
btn_x = self._rect.x + self._rect.width - (ProfileActionButton.MARGIN - ProfileActionButton.HORIZONTAL_MARGIN)
|
||||
btn_bottom = btn_y + self._rect.height
|
||||
if self._show_delete_btn:
|
||||
btn_x -= self._delete_btn.rect.width
|
||||
self._delete_btn.render(rl.Rectangle(
|
||||
btn_x, btn_bottom - self._delete_btn.rect.height,
|
||||
self._delete_btn.rect.width, self._delete_btn.rect.height,
|
||||
))
|
||||
if self._rename_btn is not None:
|
||||
btn_x -= self._rename_btn.rect.width
|
||||
self._rename_btn.render(rl.Rectangle(
|
||||
btn_x, btn_bottom - self._rename_btn.rect.height,
|
||||
self._rename_btn.rect.width, self._rename_btn.rect.height,
|
||||
))
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
def action_pressed() -> bool:
|
||||
return self._delete_btn.is_pressed or (self._rename_btn is not None and self._rename_btn.is_pressed)
|
||||
super().set_touch_valid_callback(lambda: touch_callback() and not action_pressed())
|
||||
self._delete_btn.set_touch_valid_callback(touch_callback)
|
||||
if self._rename_btn:
|
||||
self._rename_btn.set_touch_valid_callback(touch_callback)
|
||||
|
||||
|
||||
class EsimErrorDialog(NavRawScrollPanel):
|
||||
def __init__(self, error: str):
|
||||
super().__init__()
|
||||
self._title = UnifiedLabel("esim error", font_size=64, font_weight=FontWeight.BOLD)
|
||||
self._error = UnifiedLabel(error, font_size=36, elide=False)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
width = int(rect.width - 80)
|
||||
title_height = self._title.get_content_height(width)
|
||||
error_height = self._error.get_content_height(width)
|
||||
offset = self._scroll_panel.update(rect, title_height + error_height + 100)
|
||||
y = rect.y + 40 + offset
|
||||
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._title.render(rl.Rectangle(rect.x + 40, y, width, title_height))
|
||||
self._error.render(rl.Rectangle(rect.x + 40, y + title_height + 20, width, error_height))
|
||||
rl.end_scissor_mode()
|
||||
|
||||
|
||||
class EsimUI(NavScroller):
|
||||
def __init__(self, cellular_manager: CellularManager, profiles_enabled: Callable[[], bool]):
|
||||
super().__init__()
|
||||
|
||||
self._cellular_manager = cellular_manager
|
||||
self._profiles_enabled = profiles_enabled
|
||||
|
||||
self._add_profile_btn = BigButton("add profile", "scan QR code")
|
||||
self._add_profile_btn.set_click_callback(self._on_add_profile)
|
||||
self._scroller.add_widget(self._add_profile_btn)
|
||||
self._installing_dialog: InstallingProfileDialog | None = None
|
||||
|
||||
self._cellular_manager.on_profiles_updated = self._on_profiles_updated
|
||||
self._cellular_manager.on_operation_error = self._on_error
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._update_buttons(re_sort=True)
|
||||
self._cellular_manager.refresh_profiles()
|
||||
|
||||
def _on_profiles_updated(self):
|
||||
if self._installing_dialog:
|
||||
existing = {btn.profile.iccid for btn in self._scroller.items if isinstance(btn, EsimProfileButton)}
|
||||
added = [profile for profile in self._cellular_manager.profiles if profile.iccid not in existing]
|
||||
# Start the normal tap-to-activate flow once the profile list is visible again.
|
||||
self._installing_dialog.dismiss(lambda: self._on_profile_clicked(added[0]) if len(added) == 1 else None)
|
||||
self._installing_dialog = None
|
||||
|
||||
self._update_buttons()
|
||||
|
||||
def _update_buttons(self, re_sort: bool = False):
|
||||
existing = {btn.profile.iccid: btn for btn in self._scroller.items if isinstance(btn, EsimProfileButton)}
|
||||
buttons = []
|
||||
for profile in self._cellular_manager.profiles:
|
||||
btn = existing.get(profile.iccid)
|
||||
if btn is None:
|
||||
btn = EsimProfileButton(profile, self._cellular_manager, self._profiles_enabled)
|
||||
btn.set_click_callback(lambda btn=btn: self._on_profile_clicked(btn.profile))
|
||||
self._scroller.add_widget(btn)
|
||||
else:
|
||||
btn.update_profile(profile)
|
||||
buttons.append(btn)
|
||||
|
||||
if re_sort:
|
||||
self._scroller.items[:] = sorted(buttons, key=lambda b: not b.profile.enabled)
|
||||
else:
|
||||
self._scroller.items[:] = [btn for btn in self._scroller.items if btn in buttons]
|
||||
|
||||
self._scroller.items.append(self._add_profile_btn)
|
||||
|
||||
def _move_profile_to_front(self, iccid: str | None, scroll: bool = False):
|
||||
front_btn_idx = next((i for i, btn in enumerate(self._scroller.items)
|
||||
if isinstance(btn, EsimProfileButton) and btn.profile.iccid == iccid), None) if iccid else None
|
||||
|
||||
if front_btn_idx is not None and front_btn_idx > 0:
|
||||
self._scroller.move_item(front_btn_idx, 0)
|
||||
|
||||
if scroll:
|
||||
self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self._add_profile_btn.set_enabled(not self._cellular_manager.busy and self._profiles_enabled())
|
||||
active = self._cellular_manager.active_profile
|
||||
self._move_profile_to_front(active.iccid if active else None)
|
||||
|
||||
def _on_add_profile(self):
|
||||
if self._cellular_manager.busy or not self._profiles_enabled():
|
||||
return
|
||||
if ui_state.sm["deviceState"].networkType == log.DeviceState.NetworkType.none:
|
||||
gui_app.push_widget(BigDialog("", tr("Ensure you're connected to the internet and try again.")))
|
||||
return
|
||||
gui_app.push_widget(QRScannerDialog(on_qr_detected=self._on_qr_scanned))
|
||||
|
||||
def _on_qr_scanned(self, lpa_code: str):
|
||||
dlg = BigInputDialog("enter a nickname...", text_validator=lambda text: bool(text.strip()),
|
||||
confirm_callback=lambda nickname: self._download_profile(lpa_code, nickname))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _download_profile(self, lpa_code: str, nickname: str):
|
||||
self._installing_dialog = InstallingProfileDialog()
|
||||
gui_app.push_widget(self._installing_dialog)
|
||||
self._cellular_manager.download_profile(lpa_code, nickname.strip())
|
||||
|
||||
def _on_error(self, error: str):
|
||||
cloudlog.error("eSIM error: %s", error)
|
||||
dlg = EsimErrorDialog(error)
|
||||
if self._installing_dialog:
|
||||
self._installing_dialog.dismiss(lambda: gui_app.push_widget(dlg))
|
||||
self._installing_dialog = None
|
||||
else:
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _on_profile_clicked(self, profile: Profile):
|
||||
if self._cellular_manager.busy or not self._profiles_enabled():
|
||||
return
|
||||
self._cellular_manager.switch_profile(profile.iccid)
|
||||
self._move_profile_to_front(profile.iccid, scroll=True)
|
||||
@@ -1,13 +1,12 @@
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import EsimNetworkButton, WifiNetworkButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.esim_ui import EsimUI
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.cellular_manager import CellularManager
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavScroller):
|
||||
@@ -29,8 +28,7 @@ class NetworkLayoutMici(NavScroller):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback,
|
||||
description="Share the device’s internet connection through a Wi-Fi hotspot.")
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
@@ -60,39 +58,26 @@ class NetworkLayoutMici(NavScroller):
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback,
|
||||
description="Metered prevents large uploads on this Wi-Fi connection. Default uses the network’s detected " +
|
||||
"setting.")
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
self._wifi_button = WifiNetworkButton(self._wifi_manager)
|
||||
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
|
||||
|
||||
# ******** eSIM ********
|
||||
self._cellular_manager = CellularManager()
|
||||
self._esim_ui = EsimUI(
|
||||
self._cellular_manager,
|
||||
lambda: not ui_state.prime_state.is_full_prime(),
|
||||
)
|
||||
self._esim_button = EsimNetworkButton(self._cellular_manager)
|
||||
self._esim_button.set_click_callback(lambda: gui_app.push_widget(self._esim_ui))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", description="Allow cellular data roaming.")
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming")
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit",
|
||||
description="Set the access point name required by your cellular carrier. Leave blank for automatic configuration.")
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", description="Prevent large uploads over the cellular connection.")
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered")
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller.add_widgets([
|
||||
self._wifi_button,
|
||||
self._esim_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
@@ -106,7 +91,8 @@ class NetworkLayoutMici(NavScroller):
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
show_cell_settings = not ui_state.prime_state.is_full_prime()
|
||||
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
|
||||
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
|
||||
self._wifi_manager.set_ipv4_forward(show_cell_settings)
|
||||
self._roaming_btn.set_visible(show_cell_settings)
|
||||
self._apn_btn.set_visible(show_cell_settings)
|
||||
@@ -116,16 +102,14 @@ class NetworkLayoutMici(NavScroller):
|
||||
super().show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
|
||||
# Process wifi and esim callbacks while at any point in the nav stack
|
||||
# Process wifi callbacks while at any point in the nav stack
|
||||
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
gui_app.add_nav_stack_tick(self._cellular_manager.process_callbacks)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_manager.set_active(False)
|
||||
|
||||
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
gui_app.remove_nav_stack_tick(self._cellular_manager.process_callbacks)
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
|
||||
@@ -3,7 +3,7 @@ from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
|
||||
@@ -49,6 +49,7 @@ class SettingsLayout(NavScroller):
|
||||
network_btn,
|
||||
device_btn,
|
||||
software_btn,
|
||||
PairBigButton(),
|
||||
firehose_btn,
|
||||
developer_btn,
|
||||
])
|
||||
|
||||
@@ -74,10 +74,6 @@ class SoftwareInfoLayoutMici(Widget):
|
||||
|
||||
|
||||
class CheckUpdateButton(BigButton):
|
||||
UPDATER_PROC = "openpilot.system.updated.updated"
|
||||
CHECK_FOR_UPDATE = "SIGUSR1"
|
||||
DOWNLOAD_UPDATE = "SIGHUP"
|
||||
|
||||
def __init__(self):
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
@@ -101,20 +97,15 @@ class CheckUpdateButton(BigButton):
|
||||
gui_app.push_widget(dlg)
|
||||
return
|
||||
|
||||
self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE)
|
||||
|
||||
def check_for_update(self):
|
||||
self._signal_updater(self.CHECK_FOR_UPDATE)
|
||||
|
||||
def _signal_updater(self, sig: str):
|
||||
self.set_enabled(False)
|
||||
self._state = UpdaterState.WAITING_FOR_UPDATER
|
||||
self._hide_value_t = None
|
||||
self.set_value("")
|
||||
self.set_icon(self._txt_update_icon)
|
||||
|
||||
def run():
|
||||
subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True)
|
||||
if self.get_value() == "download update":
|
||||
subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True)
|
||||
else:
|
||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
@@ -193,7 +184,7 @@ class CheckUpdateButton(BigButton):
|
||||
|
||||
class InstallUpdateButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
||||
|
||||
def _update_state(self):
|
||||
@@ -241,10 +232,8 @@ class BranchSelectPage(NavScroller):
|
||||
|
||||
|
||||
class TargetBranchButton(BigButton):
|
||||
def __init__(self, check_update_btn: CheckUpdateButton):
|
||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "",
|
||||
description="Select the software branch to download on the next update check.")
|
||||
self._check_update_btn = check_update_btn
|
||||
def __init__(self):
|
||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self.set_click_callback(self._on_click)
|
||||
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self.set_enabled(lambda: ui_state.is_offroad())
|
||||
@@ -257,15 +246,12 @@ class TargetBranchButton(BigButton):
|
||||
self.set_value(target)
|
||||
|
||||
def _on_click(self):
|
||||
if not ui_state.params.get("UpdaterAvailableBranches"):
|
||||
gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again.")))
|
||||
return
|
||||
gui_app.push_widget(BranchSelectPage(self._on_select))
|
||||
|
||||
def _on_select(self, branch: str):
|
||||
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
|
||||
self.set_value(branch)
|
||||
self._check_update_btn.check_for_update()
|
||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||
|
||||
|
||||
class SoftwareLayoutMici(NavScroller):
|
||||
@@ -277,15 +263,12 @@ class SoftwareLayoutMici(NavScroller):
|
||||
|
||||
uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall",
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
uninstall_openpilot_callback, exit_on_confirm=False,
|
||||
description="Remove openpilot from this device.",
|
||||
description_icon=gui_app.texture("icons_mici/setup/factory_reset.png", 64, 64))
|
||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||
|
||||
check_update_btn = CheckUpdateButton()
|
||||
self._scroller.add_widgets([
|
||||
SoftwareInfoLayoutMici(),
|
||||
check_update_btn,
|
||||
CheckUpdateButton(),
|
||||
InstallUpdateButton(),
|
||||
TargetBranchButton(check_update_btn),
|
||||
TargetBranchButton(),
|
||||
uninstall_openpilot_btn,
|
||||
])
|
||||
|
||||
@@ -41,34 +41,15 @@ class TogglesLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"],
|
||||
description="Standard is recommended.\n" +
|
||||
"Aggressive follows closer, with firmer gas and braking.\n" +
|
||||
"Relaxed leaves more space.\n" +
|
||||
"Use the steering wheel distance button on supported cars.")
|
||||
self._experimental_btn = BigToggle("experimental mode", description_icon=gui_app.texture("icons_mici/experimental_mode.png", 64, 64),
|
||||
initial_state=ui_state.params.get_bool("ExperimentalMode"), toggle_callback=self._on_experimental_mode,
|
||||
description="Let the driving model control gas and brakes.\n" +
|
||||
"Includes stopping for red lights and stop signs.\n" +
|
||||
"Set speed is a maximum, not a target.\n" +
|
||||
"These are alpha features. Expect mistakes.\n" +
|
||||
"The path colors show acceleration and braking.")
|
||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
|
||||
self._experimental_btn = BigToggle("experimental mode", initial_state=ui_state.params.get_bool("ExperimentalMode"),
|
||||
toggle_callback=self._on_experimental_mode)
|
||||
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled",
|
||||
description="Warn when you drift across a detected lane line.\n" +
|
||||
"Only above 31 mph (50 km/h), with no turn signal.")
|
||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM", description="Monitor the driver even when sunnypilot is not engaged.")
|
||||
record_front = BigParamControl("record & upload cabin camera", "RecordFront",
|
||||
description_icon=gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
|
||||
toggle_callback=restart_needed_callback, description="Upload cabin camera data to help improve driver monitoring.")
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", description_icon=gui_app.texture("icons_mici/microphone.png", 64, 64),
|
||||
toggle_callback=restart_needed_callback,
|
||||
description="Record microphone audio while driving.\n" +
|
||||
"Audio is included in dashcam videos in sunnylink.")
|
||||
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback,
|
||||
description="Enable to use sunnypilot driver assistance.\n" +
|
||||
"Disable to use your car's stock driver assistance.")
|
||||
|
||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled")
|
||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
||||
record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback)
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._personality_toggle,
|
||||
|
||||
@@ -10,7 +10,7 @@ from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||
from openpilot.common.hardware import COMMA_HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
@@ -333,7 +333,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._alert_text1_label.set_text(alert_text1)
|
||||
self._alert_text1_label.set_text_color(color)
|
||||
self._alert_text1_label.set_font_size(font_size)
|
||||
self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
||||
self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
self._alert_text1_label.render(text_rect1)
|
||||
|
||||
alert_text2 = alert.text2.lower()
|
||||
@@ -365,5 +365,5 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._alert_text2_label.set_text(alert_text2)
|
||||
self._alert_text2_label.set_text_color(color)
|
||||
self._alert_text2_label.set_font_size(small_font_size)
|
||||
self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
||||
self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
self._alert_text2_label.render(text_rect2)
|
||||
|
||||
@@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
@@ -158,8 +158,8 @@ class AugmentedRoadView(CameraView):
|
||||
self._confidence_ball = ConfidenceBall()
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
@@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget):
|
||||
|
||||
if not self._camera_view.frame:
|
||||
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
||||
alignment=TextAlignment.CENTER)
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
rl.end_scissor_mode()
|
||||
self._publish_alert_sound(None)
|
||||
return
|
||||
@@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget):
|
||||
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
|
||||
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
|
||||
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||
alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
color=rl.Color(0, 0, 0, 180))
|
||||
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||
alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
|
||||
@@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget):
|
||||
|
||||
# Show alert level
|
||||
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
||||
alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT
|
||||
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
|
||||
|
||||
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
||||
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||
alignment=alignment,
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||
color=rl.Color(0, 0, 0, 180))
|
||||
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||
alignment=alignment,
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
def _load_eye_textures(self):
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -29,41 +29,9 @@ class ScrollState(Enum):
|
||||
POST_SCROLL = 2
|
||||
|
||||
|
||||
class BaseButton(Widget):
|
||||
def __init__(self, description: str, title: str, icon: Union[rl.Texture, None] = None):
|
||||
class BigCircleButton(Widget):
|
||||
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__()
|
||||
self._shake_start: float | None = None
|
||||
if description:
|
||||
# Dialogs also use buttons; import lazily to avoid a circular import.
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import SettingDescriptionDialog
|
||||
self.set_long_press_callback(lambda: gui_app.push_widget(SettingDescriptionDialog(title, description, icon)))
|
||||
else:
|
||||
self.set_long_press_callback(self.trigger_shake)
|
||||
|
||||
def trigger_shake(self):
|
||||
self._shake_start = rl.get_time()
|
||||
|
||||
@property
|
||||
def _shake_offset(self) -> float:
|
||||
SHAKE_DURATION = 0.5
|
||||
SHAKE_AMPLITUDE = 24.0
|
||||
SHAKE_FREQUENCY = 32.0
|
||||
if self._shake_start is None:
|
||||
return 0.0
|
||||
t = rl.get_time() - self._shake_start
|
||||
if t > SHAKE_DURATION:
|
||||
return 0.0
|
||||
decay = 1.0 - t / SHAKE_DURATION
|
||||
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
super().set_position(x + self._shake_offset, y)
|
||||
|
||||
class BigCircleButton(BaseButton):
|
||||
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0),
|
||||
*, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None, title: str = ""):
|
||||
super().__init__(description, title, description_icon or icon)
|
||||
self._red = red
|
||||
self._icon_offset = icon_offset
|
||||
|
||||
@@ -105,9 +73,8 @@ class BigCircleButton(BaseButton):
|
||||
|
||||
|
||||
class BigCircleToggle(BigCircleButton):
|
||||
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0),
|
||||
*, description: str = "", description_icon: Union[rl.Texture, None] = None, title: str = ""):
|
||||
super().__init__(icon, False, icon_offset=icon_offset, description=description, description_icon=description_icon, title=title)
|
||||
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, False, icon_offset=icon_offset)
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
# State
|
||||
@@ -136,16 +103,14 @@ class BigCircleToggle(BigCircleButton):
|
||||
0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class BigButton(BaseButton):
|
||||
class BigButton(Widget):
|
||||
LABEL_HORIZONTAL_PADDING = 40
|
||||
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
|
||||
|
||||
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
|
||||
|
||||
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False,
|
||||
*, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(description, text, description_icon or icon or None)
|
||||
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 402, 180))
|
||||
self.text = text
|
||||
self.value = value
|
||||
@@ -154,15 +119,16 @@ class BigButton(BaseButton):
|
||||
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._click_delay = 0.075
|
||||
self._shake_start: float | None = None
|
||||
self._grow_animation_until: float | None = None
|
||||
|
||||
self._rotate_icon_t: float | None = None
|
||||
|
||||
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
||||
text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll,
|
||||
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
|
||||
line_height=0.9)
|
||||
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
||||
text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM)
|
||||
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._update_label_layout()
|
||||
|
||||
self._load_images()
|
||||
@@ -201,9 +167,9 @@ class BigButton(BaseButton):
|
||||
def _update_label_layout(self):
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
if self.value:
|
||||
self._label.set_alignment_vertical(TextAlignmentVertical.TOP)
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
else:
|
||||
self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM)
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
|
||||
def set_text(self, text: str):
|
||||
self.text = text
|
||||
@@ -221,9 +187,28 @@ class BigButton(BaseButton):
|
||||
def get_text(self):
|
||||
return self.text
|
||||
|
||||
def trigger_shake(self):
|
||||
self._shake_start = rl.get_time()
|
||||
|
||||
def trigger_grow_animation(self, duration: float = 0.65):
|
||||
self._grow_animation_until = rl.get_time() + duration
|
||||
|
||||
@property
|
||||
def _shake_offset(self) -> float:
|
||||
SHAKE_DURATION = 0.5
|
||||
SHAKE_AMPLITUDE = 24.0
|
||||
SHAKE_FREQUENCY = 32.0
|
||||
if self._shake_start is None:
|
||||
return 0.0
|
||||
t = rl.get_time() - self._shake_start
|
||||
if t > SHAKE_DURATION:
|
||||
return 0.0
|
||||
decay = 1.0 - t / SHAKE_DURATION
|
||||
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
super().set_position(x + self._shake_offset, y)
|
||||
|
||||
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
|
||||
if self._grow_animation_until is not None:
|
||||
if rl.get_time() >= self._grow_animation_until:
|
||||
@@ -287,10 +272,8 @@ class BigButton(BaseButton):
|
||||
|
||||
|
||||
class BigToggle(BigButton):
|
||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None,
|
||||
*, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(text, value, "", description=description, description_icon=description_icon)
|
||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None):
|
||||
super().__init__(text, value, "")
|
||||
self._checked = initial_state
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
@@ -325,8 +308,8 @@ class BigToggle(BigButton):
|
||||
|
||||
class BigMultiToggle(BigToggle):
|
||||
def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None, *, description: str = "", description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(text, "", toggle_callback=toggle_callback, description=description, description_icon=description_icon)
|
||||
select_callback: Callable | None = None):
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
assert len(options) > 0
|
||||
self._options = options
|
||||
self._select_callback = select_callback
|
||||
@@ -373,8 +356,8 @@ class GreyBigButton(BigButton):
|
||||
self._sub_label.set_font_size(36)
|
||||
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||
self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else
|
||||
TextAlignmentVertical.BOTTOM)
|
||||
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
|
||||
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._sub_label.set_line_height(0.95)
|
||||
|
||||
@property
|
||||
@@ -391,9 +374,9 @@ class GreyBigButton(BigButton):
|
||||
|
||||
class BigMultiParamToggle(BigMultiToggle):
|
||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None, *, description: str = "", description_icon: Union[rl.Texture, None] = None):
|
||||
select_callback: Callable | None = None):
|
||||
assert Params is not None
|
||||
super().__init__(text, options, toggle_callback, select_callback, description=description, description_icon=description_icon)
|
||||
super().__init__(text, options, toggle_callback, select_callback)
|
||||
self._param = param
|
||||
|
||||
self._params = Params()
|
||||
@@ -409,10 +392,9 @@ class BigMultiParamToggle(BigMultiToggle):
|
||||
|
||||
|
||||
class BigParamControl(BigToggle):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None, *, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||
assert Params is not None
|
||||
super().__init__(text, "", toggle_callback=toggle_callback, description=description, description_icon=description_icon)
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
self.param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self.params.get_bool(self.param, False))
|
||||
@@ -428,9 +410,9 @@ class BigParamControl(BigToggle):
|
||||
# TODO: param control base class
|
||||
class BigCircleParamControl(BigCircleToggle):
|
||||
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
|
||||
icon_offset: tuple[int, int] = (0, 0), *, description: str = "", description_icon: Union[rl.Texture, None] = None, title: str = ""):
|
||||
icon_offset: tuple[int, int] = (0, 0)):
|
||||
assert Params is not None
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset, description=description, description_icon=description_icon, title=title)
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
|
||||
self._param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self.params.get_bool(self._param, False))
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import abc
|
||||
import math
|
||||
import re
|
||||
import pyray as rl
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -39,31 +37,6 @@ class BigDialog(BigDialogBase):
|
||||
))
|
||||
|
||||
|
||||
class SettingDescriptionDialog(NavScroller):
|
||||
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None):
|
||||
super().__init__()
|
||||
cards = [GreyBigButton(title, "scroll for details", icon or gui_app.texture("icons_mici/setup/green_info.png", 64, 64))]
|
||||
# Explicit lines are authored cards; otherwise prefer sentence boundaries.
|
||||
paragraphs = description.splitlines() if "\n" in description else re.split(r"(?<=[.!?])\s+", description.strip())
|
||||
# Measure each card so longer text still fits with the actual font and padding.
|
||||
for sentence in paragraphs:
|
||||
card = GreyBigButton("", "")
|
||||
words: list[str] = []
|
||||
for word in sentence.split():
|
||||
card.set_value(" ".join([*words, word]))
|
||||
height = card._sub_label.get_content_height(card._subtitle_width_hint())
|
||||
if words and height > card.rect.height - 2 * card.LABEL_VERTICAL_PADDING:
|
||||
card.set_value(" ".join(words))
|
||||
cards.append(card)
|
||||
card = GreyBigButton("", "")
|
||||
words = []
|
||||
words.append(word)
|
||||
if words:
|
||||
card.set_value(" ".join(words))
|
||||
cards.append(card)
|
||||
self._scroller.add_widgets(cards)
|
||||
|
||||
|
||||
class BigConfirmationDialog(BigDialogBase):
|
||||
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
@@ -103,15 +76,14 @@ class BigInputDialog(BigDialogBase):
|
||||
default_text: str = "",
|
||||
minimum_length: int = 1,
|
||||
confirm_callback: Callable[[str], None] | None = None,
|
||||
auto_return_to_letters: str = "",
|
||||
text_validator: Callable[[str], bool] | None = None):
|
||||
auto_return_to_letters: str = ""):
|
||||
super().__init__()
|
||||
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
|
||||
font_weight=FontWeight.MEDIUM)
|
||||
self._keyboard = MiciKeyboard(auto_return_to_letters=auto_return_to_letters)
|
||||
self._keyboard.set_text(default_text)
|
||||
self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
|
||||
self._text_valid = lambda text: len(text) >= minimum_length and (text_validator is None or text_validator(text))
|
||||
self._minimum_length = minimum_length
|
||||
|
||||
self._backspace_held_time: float | None = None
|
||||
|
||||
@@ -128,8 +100,7 @@ class BigInputDialog(BigDialogBase):
|
||||
|
||||
def confirm_callback_wrapper():
|
||||
text = self._keyboard.text()
|
||||
if self._text_valid(text):
|
||||
self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None)
|
||||
self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None)
|
||||
self._confirm_callback = confirm_callback_wrapper
|
||||
|
||||
def _update_state(self):
|
||||
@@ -214,7 +185,7 @@ class BigInputDialog(BigDialogBase):
|
||||
self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height)
|
||||
|
||||
# draw enter button
|
||||
self._enter_img_alpha.update(255 if self._text_valid(text) else 0)
|
||||
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
|
||||
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
|
||||
rl.draw_texture_ex(self._enter_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
|
||||
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||
from openpilot.cereal import messaging, log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.common.hardware import COMMA_HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -76,10 +76,10 @@ class AlertRenderer(Widget):
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
# font size is set dynamically
|
||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER,
|
||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER,
|
||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
|
||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||
"""Generate the current alert based on selfdrive state."""
|
||||
|
||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
@@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView):
|
||||
tr("camera starting"),
|
||||
font_size=100,
|
||||
font_weight=FontWeight.BOLD,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
return -1
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user