Compare commits

...

22 Commits

Author SHA1 Message Date
discountchubbs a992d64eb6 Kumars Vibe 2026-08-14 09:00:22 -07:00
discountchubbs eff0854aad Falling Phoenix 2026-08-14 08:59:48 -07:00
Kumar 2d859a8ca6 Controls: Road Edge Lane Change Controller (#1085)
* relc

* so picky

* clean up

* switch to distance based

* same calls

* all you brother i'm just here to red diff

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-11 10:29:14 -04:00
Nayan 2a16b0fbba ui: screensaver (#1551)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* Option Control

* Need this

* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* better padding

* this

* listitem -> listitemsp

* Revert "add ui_update callback"

This reverts commit 4da32cc009.

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* simplify

* I. SAID. SIMPLIFY.

* AAARGGGGGG.....

* init

* option control value fix

* add all controls

* hide all controls

* lint

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* optimizations

* Removed hide for now

* why? because!

* oops

* better

* add param, timeout

* add toggle

* not doing this

* we doing this now @devtekve? FINE!!

* it wasn't me

* really @devtekve? REALLY??

* refresh controls

* ugh

* changes

* fix

* inline everything again

* handle default

* use ui_state param

* better

* fix

* lint

* better

* solve for lint

* default on pls

* screen saver hue distance and increase minimum color shift

* debounce screen saver color changes

* fix dismiss flow

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: DevTekVE <devtekve@gmail.com>
2026-08-10 04:27:30 -04:00
Jason Wen c6595fd99b offroad alerts: replace comma references with sunnypilot community and sunnylink (#1904) 2026-08-09 23:12:51 -04:00
github-actions[bot] 617fcd4084 [bot] Update Python packages (#1866)
* Update Python packages

* no

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-09 20:22:41 -04:00
Jason Wen 91a53aa161 Controls: Lateral Jerk Torque Controller (#693)
* init

* more init

* keep it alive

* fixes

* more fixes

* more fix

* new submodule for nn data

* bump submodule

* update path to submodule

* spacing???

* update submodule path

* update submodule path

* bump

* dump

* bump

* introduce params

* Add Neural Network Lateral Control toggle to developer panel

This introduces a new toggle for enabling Neural Network Lateral Control (NNLC), providing detailed descriptions of its functionality and compatibility. It includes UI integration, car compatibility checks, and feedback links for unsupported vehicles.

* decouple even more

* static

* codespell

* remove debug

* in structs

* fix import

* convert to capnp

* fixes

* debug

* only initialize if NNLC is enabled or allow to enable

* oops

* fix initialization

* only allow engage if nnlc is off

* fix toggle param

* fix tests

* lint

* fix more test

* capnp test

* try this out

* validate if it's not None

* make it 33 to match

* align

* share the same friction input calculation

* return stock values if not enabled

* unused

* split base and child

* space

* rename

* NeuralNetworkFeedForwardModel

* less

* just use file name

* try this

* more explicit

* rename

* move it

* child class for additional controllers

* rename

* time to split out custom lateral acceleration

* move around

* space

* fix

* TODO-SP

* TODO-SP

* update regardless, it's an extension now

* update name and expose toggle

* ui: sunnypilot Panel -> Steering Panel

* Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h

* merge

* move to steering panel

* no need for this

* live params in a thread

* no live for now

* new structs

* more ui

* more flexible

* more ui

* no longer needed

* another ui

* cereal changes

* bump opendbc

* simplify checks

* all in one place

* just Enhanced Lat Accel only

* no submodule for this

* Enhanced Lateral Acceleration: fix bugs, restore NNLC, add UI/schema

Fix 4 bugs in latcontrol_torque_lat_accel.py: missing CI param, wrong
method name (torque_from_lateral_accel → torque_from_lateral_accel_in_torque_space),
self.enabled collision with NNLC, missing output torque recomputation.

Restore NNLC + ExtOverride inheritance chain with Enhanced slotted between
ExtBase and NNLC. Add mutual exclusion constraints, Python UI toggle,
sunnylink schema entries, and controller init/update tests.

* fix lint: remove unused default params list, add ty ignore, fix test fixture

* use the og name

* rename

* need gates

* TODOs

* wrong

* gate them all on init

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
2026-08-09 15:56:33 -04:00
Jason Wen fc4699a747 controls: abstract update_output_torque to ExtBase (#1903) 2026-08-09 15:19:35 -04:00
Jason Wen 9e32dee281 ci: fix process replay with missing sunnypilot service ignores (#1902) 2026-08-09 03:47:59 -04:00
Jason Wen f531952be3 ci: sunnypilot process replay (#1900)
* ci: sunnypilot process replay

* ours

* temp

* Revert "temp"

This reverts commit d198cbb327.

* fixes, hopefully

* bump
2026-08-08 19:49:06 -04:00
Jason Wen 6f2a1d573c ci: sunnypilot CI test routes (#1899) 2026-08-08 01:27:33 -04:00
MVL 9e0d89968d Honda Clarity: brake hold fix (#1247)
* Nidec Hybrid brake hold fix

* intent fix

* bump opendbc

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-06 22:18:37 -04:00
Jason Wen fec5a97a9e docs: update policy (#1898) 2026-08-06 21:57:56 -04:00
Jason Wen 099143ad9d [mici] ui: fix swipe-down in sunnylink and models panels (#1896)
* [mici] ui: fix swipe-down in sunnylink and models panels

* update mici replay test

* [mici] ui: push model sub-views as separate widgets for correct back navigation
2026-08-05 23:22:36 -04:00
Jason Wen 6909aa95ff ui: use sunnypilot and sunnylink branding fonts (#1895)
* ui: use sunnypilot and sunnylink branding fonts

* split
2026-08-05 22:53:04 -04:00
Nayan 5bdc0c23a9 modeld_v2: Support Deep Models (#1887)
* modeld_v2: safe model validation

* fix string

* numpy

* dumb

* god use full attribute names please

* modeld_v2: refactor compile_modeld

* redundant

* CREAM AND SUGAR

* gpu stuffs

* Update fetcher.py

* Update compile_modeld.py

* Update compile_modeld.py

* Update compile_modeld.py

* Update compile_modeld.py

* summary

* Update compile_modeld.py

* Update compile_modeld.py

* Update compile_modeld.py

* i could lie say

* needed

* i could

* Update compile_modeld.py

* done done done

* bye metadata

* simplify

* deeeeep

* oopsie

* i don't know what i'm doing

* it's a supercombo

* wtf. ghostwriter

* i might be blind

* fuck. i AM blind. or dumb. or both.

* hmmmm

* read/open - what's the difference

* realize. that i don't know shit

* whatever

* fix paths

* fuckit. dynamic everything.

* simplify everything.

* fix macos cabana

* np.random.seed is deprecated, use Generator or default rng instead @nayan

* this is annoying me

* too much fluff

* no

* lint be crazy now, man i've been away a while

* back

* fix pkl loader test

* that comment was wrong, its still per model, just compiled at with the input/output shapes

* fix chunking

* dump and assign to cpu

* prev_feat in cpu to prevent corruption

* add todo-sp

* desire me?

* allow legacy

* lint

* shapey

* bye

---------

Co-authored-by: discountchubbs <alexgrant990@gmail.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-05 12:30:28 -07:00
Amy Jeanes 1a07e47228 Tesla: MADS Screen Activation (#1808)
* Tesla: MADS Screen Button Settings

Adds a Tesla vehicle setting (vehicle bus required) to control how many
fingers activate the MADS screen button, or disable it entirely.

Rebased onto current master:
- Migrated the setting metadata from the deprecated params_metadata.json
  (removed in #1862) to the yaml SDUI system: added TeslaMadsScreenButton
  to settings_ui_src/pages/vehicle.yaml and recompiled settings_ui.json.
  Vehicle-bus gating uses the tesla_has_vehicle_bus capability visibility.
- Bumped opendbc_repo to the latest head of sunnypilot/opendbc#459.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JiSH2KAAueUmDe29xvpuAd

* bump

* Tesla: address MADS Screen Activation review feedback

Default TeslaMadsScreenButton to Off for fresh installs and add a param
migration that seeds existing Tesla installs with 3-Finger, preserving the
previous always-on behaviour. Brand resolves from CarPlatformBundle, falling
back to CarParamsPersistent so auto-fingerprinted Teslas are covered too.

Rename the setting to "MADS Screen Activation", hyphenate the finger-count
labels, and reword the description to use <br> (descriptions render as HTML)
with a note that a higher finger count may reduce accidental activations.
Applied both on-device and in sunnylink.

Also fix test_tesla_with_vehicle_bus_uses_param, which broke once
get_mads_limited_brands started reading TeslaMadsScreenButton from the same
blanket params mock, and add coverage for the screen-button-Off path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5icDp7zZ49gpyC2mpCfF1

* bump opendbc

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-01 23:40:52 -04:00
Jason Wen 50b860c928 Reapply "plannerd: check all services for validity (#38341)"
This reverts commit 0265ae5f76.
2026-08-01 23:27:36 -04:00
Jason Wen 978ec800fe plannerd & selfdriveStateSP: poll modelV2, relay button state via bitmask (#1893)
* selfdrived: continuous button state and release counters in selfdriveStateSP

* tests and more
2026-08-01 23:05:03 -04:00
Jason Wen 3a05c03079 ci: no more docker (#1886) 2026-07-25 15:34:36 -04:00
Eitan fd22de1c9a SCC-M: fix operator precedence in quadratic roots (#1816)
* controls/scc: fix operator precedence in map controller quadratic roots

* not async

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-07-25 09:37:18 -04:00
Christopher Haucke ee3583df33 [tizi/tici] ui: Camera offset controls (#1813)
* Camera offset controls

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-07-25 09:15:05 -04:00
98 changed files with 2215 additions and 1659 deletions
@@ -12,11 +12,11 @@ on:
required: false required: false
type: string type: string
recompiled_dir: recompiled_dir:
description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' description: 'Existing recompiled directory number (e.g. 1 for recompiled1)'
required: true required: true
type: string type: string
json_version: json_version:
description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' description: 'driving_models version number to update (e.g. 18 for driving_models_v18.json)'
required: true required: true
type: string type: string
artifact_suffix: artifact_suffix:
@@ -63,12 +63,11 @@ on:
default: 'None' default: 'None'
options: options:
- None - None
- Simple Plan Models - Master Models
- Space Lab Models - Release Models
- TR Models - 2025 World Models
- DTR Models - 2026 World Models
- Custom Merge Models - Custom Merge Models
- FOF series models
- Other - Other
custom_model_folder: custom_model_folder:
description: 'Custom model folder name (if "Other" selected)' description: 'Custom model folder name (if "Other" selected)'
-39
View File
@@ -1,39 +0,0 @@
name: prebuilt
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
env:
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: release/ci/docker_build_sp.sh
jobs:
build_prebuilt:
name: build prebuilt
runs-on: ubuntu-latest
if: github.repository == 'sunnypilot/sunnypilot'
env:
PUSH_IMAGE: true
permissions:
checks: read
contents: read
packages: write
steps:
- name: Wait for green check mark
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
wait-interval: 30
running-workflow-name: 'build prebuilt'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build master-ci|create badges).*).)*$
- uses: actions/checkout@v6
with:
submodules: true
- run: git lfs pull
- name: Build and Push docker image
run: |
$DOCKER_LOGIN
eval "$BUILD"
+35 -14
View File
@@ -30,6 +30,11 @@ on:
required: false required: false
type: string type: string
default: '' default: ''
target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)'
required: false
type: string
default: 'qcom'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
upstream_branch: upstream_branch:
@@ -46,6 +51,14 @@ on:
required: false required: false
type: boolean type: boolean
default: true default: true
target_hardware:
description: 'Hardware target to compile for'
required: true
type: choice
options:
- qcom
- usbgpu
default: 'qcom'
run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}] run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}]
@@ -161,19 +174,30 @@ jobs:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }} name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
path: ${{ env.MODELS_DIR }} path: ${{ env.MODELS_DIR }}
- run: | - run: |
rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision,big_driving_supercombo}.onnx
- name: Build Model - name: Build Model
run: | run: |
source /etc/profile source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ env.TINYGRAD_PATH }}:${{ github.workspace }}" export PYTHONPATH="${PYTHONPATH}:${{ env.TINYGRAD_PATH }}:${{ github.workspace }}"
COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py" COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py"
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build"
export USBGPU=1
TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else
echo "QCOM build"
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
fi
# Generate metadata for all ONNX files # Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
@@ -186,7 +210,13 @@ jobs:
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx" POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx"
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx" OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx"
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx" ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx"
SUPERCOMBO_ONNX="${{ env.MODELS_DIR }}/supercombo.onnx" SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do
if [ -f "$f" ]; then
SUPERCOMBO_ONNX="$f"
break
fi
done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME="" MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
if [ -f "$VISION_ONNX" ]; then if [ -f "$VISION_ONNX" ]; then
@@ -207,24 +237,15 @@ jobs:
fi fi
if [ -n "$MODEL_TYPE" ]; then if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> driving_tinygrad.pkl" echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \ env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \ --model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \ --model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \ --camera-resolutions $CAMERA_RES \
$ONNX_ARGS \ $ONNX_ARGS \
--output "${{ env.MODELS_DIR }}/driving_tinygrad.pkl" --output "$OUTPUT_PKL"
fi fi
- name: Validate Model Outputs
run: |
source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
python3 "${{ github.workspace }}/release/ci/model_generator.py" \
--validate-only \
--model-dir "${{ env.MODELS_DIR }}"
- name: Prepare Output - name: Prepare Output
run: | run: |
sudo rm -rf ${{ env.OUTPUT_DIR }} sudo rm -rf ${{ env.OUTPUT_DIR }}
+4 -5
View File
@@ -132,7 +132,6 @@ jobs:
process_replay: process_replay:
name: process replay name: process replay
if: false # disable process_replay for forks
runs-on: ${{ runs-on: ${{
(github.repository == 'commaai/openpilot') && (github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') || ((github.event_name != 'pull_request') ||
@@ -169,14 +168,14 @@ jobs:
name: diff_report_${{ github.event.number }} name: diff_report_${{ github.event.number }}
path: openpilot/selfdrive/test/process_replay/diff_report.txt path: openpilot/selfdrive/test/process_replay/diff_report.txt
- name: Checkout ci-artifacts - name: Checkout ci-artifacts
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
uses: actions/checkout@v7 uses: actions/checkout@v7
with: with:
repository: commaai/ci-artifacts repository: sunnypilot/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/ci-artifacts path: ${{ github.workspace }}/ci-artifacts
- name: Prepare refs - name: Prepare refs
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
working-directory: ${{ github.workspace }}/ci-artifacts working-directory: ${{ github.workspace }}/ci-artifacts
run: | run: |
git config user.name "GitHub Actions Bot" git config user.name "GitHub Actions Bot"
@@ -188,7 +187,7 @@ jobs:
git add . git add .
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit" git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
- name: Push refs - name: Push refs
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60
with: with:
timeout_minutes: 2 timeout_minutes: 2
+44
View File
@@ -0,0 +1,44 @@
# AI policy
## Why this exists
We use AI tools ourselves, so this isn't an anti-AI stance. The problem is people submitting code, issues, or comments they don't actually understand. AI makes that very easy to do, and it creates real work for reviewers who have to figure out what you meant when you can't explain it yourself.
If you're not going to put effort into understanding and verifying your submission, we're not going to put effort into reviewing it.
## The rule
You are responsible for everything you submit: code, PR descriptions, issues, bug reports, comments.
1. Understand what you submit. If a reviewer asks why you did something, you answer from your own understanding, not by re-prompting. If you can't do that, don't submit it.
2. Test your change. AI gets things wrong all the time. Run it, break it, confirm it actually works.
3. Driving fixes need real evidence. Attach a dongle ID, upload logs, and include segments that show the fix working. A route hash by itself proves nothing.
4. No AI-generated media (images, diagrams, videos) in issues or PRs.
## Disclosure
If AI tools helped you write something, say so. Add an `Assisted-by:` line in your commit message:
```
Assisted-by: GitHub Copilot
Assisted-by: Claude
```
Disclosing won't count against your PR. It helps reviewers know where to look. Hiding it and getting caught will.
## How we review
Reviewers are looking at whether you understand your own change. Can you explain it? Can you respond to feedback without re-prompting? Does your PR description say why you made the change, not just list what changed?
Good code from someone who used AI and understands what they wrote is fine. How you got there doesn't matter as long as you can stand behind it.
## What happens
Submissions that don't meet this bar get closed. If it keeps happening, you get blocked.
## Maintainers
Maintainers use AI at their discretion. They've earned that through sustained contribution and they know the codebase.
+8 -7
View File
@@ -1,10 +1,10 @@
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. ---> <!--- AUTOGENERATED FROM openpilot/selfdrive/car/CARS_template.md, DO NOT EDIT. --->
# Supported Cars # Supported Cars
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 341 Supported Cars # 342 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video| |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -78,8 +78,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Accord 2018-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Accord 2018-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C 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 Accord 2023-25">Buy Here</a></sub></details>||| |Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C 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 Accord 2023-25">Buy Here</a></sub></details>|||
|Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2018-22">Buy Here</a></sub></details>||| |Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2018-22">Buy Here</a></sub></details>|||
|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C 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 Accord Hybrid 2023-25">Buy Here</a></sub></details>||| |Honda|Accord Hybrid 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C 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 Accord Hybrid 2023-26">Buy Here</a></sub></details>|||
|Honda|City (Brazil only) 2023|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023">Buy Here</a></sub></details>||| |Honda|City (Brazil only) 2023-25|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023-25">Buy Here</a></sub></details>|||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Honda|Civic 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Honda|Civic 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
@@ -187,7 +187,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F 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=Kia Niro Plug-in Hybrid 2022">Buy Here</a></sub></details>||| |Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F 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=Kia Niro Plug-in Hybrid 2022">Buy Here</a></sub></details>|||
|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Optima 2017">Buy Here</a></sub></details>||| |Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Optima 2017">Buy Here</a></sub></details>|||
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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=Kia Optima 2019-20">Buy Here</a></sub></details>||| |Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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=Kia Optima 2019-20">Buy Here</a></sub></details>|||
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H 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=Kia Optima Hybrid 2019">Buy Here</a></sub></details>||| |Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H 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=Kia Optima Hybrid 2019">Buy Here</a></sub></details>|||
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Seltos 2021">Buy Here</a></sub></details>||| |Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Seltos 2021">Buy Here</a></sub></details>|||
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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=Kia Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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=Kia Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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=Kia Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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=Kia Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
@@ -230,14 +230,15 @@ A supported vehicle is one that just works when you install a comma device. All
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Mazda 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=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Mazda 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=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Nissan[<sup>6</sup>](#footnotes)|Altima 2019-24|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Altima 2019-24">Buy Here</a></sub></details>||| |Nissan[<sup>6</sup>](#footnotes)|Altima 2019-24|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Altima 2019-24">Buy Here</a></sub></details>|||
|Nissan[<sup>6</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Nissan[<sup>6</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Nissan[<sup>6</sup>](#footnotes)|Leaf IC 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Leaf IC 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Nissan[<sup>6</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Rogue 2018-20">Buy Here</a></sub></details>||| |Nissan[<sup>6</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|Nissan[<sup>6</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan X-Trail 2017">Buy Here</a></sub></details>||| |Nissan[<sup>6</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<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=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram 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=Ram 1500 2019-24">Buy Here</a></sub></details>||| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram 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=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram 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=Ram 2500 2020-24">Buy Here</a></sub></details>||| |Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram 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=Ram 2500 2020-24">Buy Here</a></sub></details>|||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram 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=Ram 3500 2019-22">Buy Here</a></sub></details>||| |Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram 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=Ram 3500 2019-22">Buy Here</a></sub></details>|||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>| |Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1S 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1S 2025">Buy Here</a></sub></details>||| |Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1S 2025">Buy Here</a></sub></details>|||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>| |Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1T 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1T 2025">Buy Here</a></sub></details>||| |Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<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=Rivian R1T 2025">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>||| |SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>||| |SEAT[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
+3
View File
@@ -1,3 +1,5 @@
> sunnypilot follows [commaai/openpilot](https://github.com/commaai/openpilot)'s contributing guidelines. The following applies to all contributions here.
# How to contribute # How to contribute
Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/). Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/).
@@ -35,6 +37,7 @@ All of these are examples of good PRs:
* **UI design**: we do not have a good review process for this yet * **UI design**: we do not have a good review process for this yet
* **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't. * **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't.
* **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged. * **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged.
* **AI-generated contributions**: see our [AI policy](AI_POLICY.md)
### First contribution ### First contribution
+12
View File
@@ -69,6 +69,8 @@ struct LeadData {
struct SelfdriveStateSP @0x81c2f05a394cf4af { struct SelfdriveStateSP @0x81c2f05a394cf4af {
mads @0 :ModularAssistiveDrivingSystem; mads @0 :ModularAssistiveDrivingSystem;
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement; intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
buttonsPressed @2 :UInt16;
buttonsReleaseToggle @3 :UInt16;
enum AudibleAlert { enum AudibleAlert {
none @0; none @0;
@@ -137,10 +139,16 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
eta @2 :UInt32; eta @2 :UInt32;
} }
struct Chunk {
fileName @0 :Text;
sha256 @1 :Text;
}
struct Artifact { struct Artifact {
fileName @0 :Text; fileName @0 :Text;
downloadUri @1 :DownloadUri; downloadUri @1 :DownloadUri;
downloadProgress @2 :DownloadProgress; downloadProgress @2 :DownloadProgress;
chunks @3 :List(Chunk);
} }
struct Model { struct Model {
@@ -155,6 +163,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
policy @3; policy @3;
offPolicy @4; offPolicy @4;
onPolicy @5; onPolicy @5;
chunked @6;
} }
} }
@@ -342,6 +351,7 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitChanged @21; speedLimitChanged @21;
speedLimitPending @22; speedLimitPending @22;
e2eChime @23; e2eChime @23;
laneChangeRoadEdge @24;
} }
} }
@@ -448,6 +458,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d { struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection; laneTurnDirection @0 :TurnDirection;
leftLaneChangeEdgeBlock @1 :Bool;
rightLaneChangeEdgeBlock @2 :Bool;
enum TurnDirection { enum TurnDirection {
none @0; none @0;
+5
View File
@@ -179,7 +179,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
{"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}},
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -222,6 +225,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}}, {"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}}, {"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -272,6 +276,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Torque lateral control custom params // Torque lateral control custom params
{"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}}, {"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}},
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}}, {"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
{"LateralJerkTorqueController", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}}, {"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}}, {"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}}, {"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
+3
View File
@@ -56,6 +56,9 @@ class CarSpecificEvents:
if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001: if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001:
events.add(EventName.manualRestart) events.add(EventName.manualRestart)
if CS.brakeHoldActive and CS.blockPcmEnable: # set by Nidec Hybrid which cannot resume from brakehold
events.add(EventName.belowEngageSpeed)
elif self.CP.brand == 'toyota': elif self.CP.brand == 'toyota':
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3 # TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
if self.CP.openpilotLongitudinalControl: if self.CP.openpilotLongitudinalControl:
+3 -1
View File
@@ -25,6 +25,8 @@ from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotc
from openpilot.tools.lib.file_sources import Source from openpilot.tools.lib.file_sources import Source
from openpilot.tools.lib.route import SegmentName from openpilot.tools.lib.route import SegmentName
from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source
SafetyModel = car.CarParams.SafetyModel SafetyModel = car.CarParams.SafetyModel
SteerControlType = structs.CarParams.SteerControlType SteerControlType = structs.CarParams.SteerControlType
@@ -132,7 +134,7 @@ class TestCarModelBase(unittest.TestCase):
segment_range = f"{cls.test_route.route}/{seg}" segment_range = f"{cls.test_route.route}/{seg}"
try: try:
sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source, sunnypilot_car_segments_source]
lr = LogReader(segment_range, sources=sources, sort_by_time=True) lr = LogReader(segment_range, sources=sources, sort_by_time=True)
return cls.get_testing_data_from_logreader(lr) return cls.get_testing_data_from_logreader(lr)
except (LogsUnavailable, AssertionError): except (LogsUnavailable, AssertionError):
@@ -33,7 +33,7 @@ class DesireHelper:
def get_lane_change_direction(CS): def get_lane_change_direction(CS):
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
def update(self, carstate, lateral_active, lane_change_prob): def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
self.alc.update_params() self.alc.update_params()
self.lane_turn_controller.update_params() self.lane_turn_controller.update_params()
v_ego = carstate.vEgo v_ego = carstate.vEgo
@@ -64,8 +64,8 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or ((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right)) (carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right)) ((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed) self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
@@ -158,7 +158,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
def publish(self, sm, pm): def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan') plan_send = messaging.new_message('longitudinalPlan')
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState']) plan_send.valid = sm.all_checks()
longitudinalPlan = plan_send.longitudinalPlan longitudinalPlan = plan_send.longitudinalPlan
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2'] longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
+5 -5
View File
@@ -23,25 +23,25 @@ def main():
cloudlog.info("plannerd got CarParamsSP") cloudlog.info("plannerd got CarParamsSP")
gps_location_service = get_gps_location_service(params) gps_location_service = get_gps_location_service(params)
ignore_services = ["liveMapDataSP", gps_location_service] ignore_services = ["liveMapDataSP", "carStateSP", "selfdriveStateSP", gps_location_service]
ldw = LaneDepartureWarning() ldw = LaneDepartureWarning()
longitudinal_planner = LongitudinalPlanner(CP, CP_SP) longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
'liveMapDataSP', 'carStateSP', gps_location_service], 'liveMapDataSP', 'carStateSP', 'selfdriveStateSP', gps_location_service],
poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services) poll='modelV2', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
while True: while True:
sm.update() sm.update()
longitudinal_planner.sla.update_car_state(sm['carState']) longitudinal_planner.sla.update_buttons(sm['selfdriveStateSP'].buttonsReleaseToggle)
if sm.updated['modelV2']: if sm.updated['modelV2']:
longitudinal_planner.update(sm) longitudinal_planner.update(sm)
longitudinal_planner.publish(sm, pm) longitudinal_planner.publish(sm, pm)
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl']) ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])
msg = messaging.new_message('driverAssistance') msg = messaging.new_message('driverAssistance')
msg.valid = sm.all_checks(['carState', 'carControl', 'modelV2', 'liveParameters']) msg.valid = sm.all_checks()
msg.driverAssistance.leftLaneDeparture = ldw.left msg.driverAssistance.leftLaneDeparture = ldw.left
msg.driverAssistance.rightLaneDeparture = ldw.right msg.driverAssistance.rightLaneDeparture = ldw.right
pm.send('driverAssistance', msg) pm.send('driverAssistance', msg)
+4 -1
View File
@@ -29,6 +29,7 @@ from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -213,6 +214,7 @@ def main(demo=False):
prev_action = log.ModelDataV2.Action() prev_action = log.ModelDataV2.Action()
DH = DesireHelper() DH = DesireHelper()
RELC = RoadEdgeLaneChangeController()
while True: while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame # Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -313,7 +315,8 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight] r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
size 1757355221
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b85e82079a2d31c5ce8616f2b429ccfcdcb8ebb5aefd72a62b8bd78aa9c7621
size 15583592
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b
size 60881999
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c824f68646a3b94f117f01c70dc8316fb466e05fbd42ccdba440b8a8dc86914b
size 46265993
@@ -26,7 +26,7 @@
"severity": 1 "severity": 1
}, },
"Offroad_CarUnrecognized": { "Offroad_CarUnrecognized": {
"text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.", "text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please select your vehicle manually at https://www.sunnylink.ai/. Need help? Visit https://community.sunnypilot.ai/",
"severity": 0 "severity": 0
}, },
"Offroad_Recalibration": { "Offroad_Recalibration": {
@@ -42,7 +42,7 @@
"severity": 0 "severity": 0
}, },
"Offroad_ExcessiveActuation": { "Offroad_ExcessiveActuation": {
"text": "Excessive %1 actuation detected on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", "text": "Excessive %1 actuation detected on your last drive. Please visit https://community.sunnypilot.ai/ and share your device's Dongle ID for troubleshooting.",
"severity": 1, "severity": 1,
"_comment": "Set extra field to lateral or longitudinal." "_comment": "Set extra field to lateral or longitudinal."
}, },
+13 -1
View File
@@ -30,6 +30,7 @@ from openpilot.sunnypilot import get_sanitize_int_param
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
REPLAY = "REPLAY" in os.environ REPLAY = "REPLAY" in os.environ
@@ -92,7 +93,7 @@ class SelfdriveD(CruiseHelper):
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches # TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
self.car_state_sock = messaging.sub_sock('carState', timeout=20) self.car_state_sock = messaging.sub_sock('carState', timeout=20)
ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP'] ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP', 'longitudinalPlanSP']
if SIMULATION: if SIMULATION:
ignore += ['driverCameraState', 'managerState'] ignore += ['driverCameraState', 'managerState']
if REPLAY: if REPLAY:
@@ -177,6 +178,7 @@ class SelfdriveD(CruiseHelper):
self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP) self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP)
CruiseHelper.__init__(self, self.CP) CruiseHelper.__init__(self, self.CP)
self.button_state_tracker = ButtonStateTracker()
def update_events(self, CS): def update_events(self, CS):
"""Compute onroadEvents from carState""" """Compute onroadEvents from carState"""
@@ -325,9 +327,16 @@ class SelfdriveD(CruiseHelper):
# Handle lane change # Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange: if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
direction = self.sm['modelV2'].meta.laneChangeDirection direction = self.sm['modelV2'].meta.laneChangeDirection
mdv2sp = self.sm['modelDataV2SP']
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \ if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
(CS.rightBlindspot and direction == LaneChangeDirection.right): (CS.rightBlindspot and direction == LaneChangeDirection.right):
self.events.add(EventName.laneChangeBlocked) self.events.add(EventName.laneChangeBlocked)
elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \
(mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right):
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
else: else:
if direction == LaneChangeDirection.left: if direction == LaneChangeDirection.left:
self.events.add(EventName.preLaneChangeLeft) self.events.add(EventName.preLaneChangeLeft)
@@ -597,6 +606,8 @@ class SelfdriveD(CruiseHelper):
icbm.sendButton = self.icbm.cruise_button icbm.sendButton = self.icbm.cruise_button
icbm.vTarget = self.icbm.v_target icbm.vTarget = self.icbm.v_target
self.button_state_tracker.publish(ss_sp)
self.pm.send('selfdriveStateSP', ss_sp_msg) self.pm.send('selfdriveStateSP', ss_sp_msg)
# onroadEventsSP - logged every second or on change # onroadEventsSP - logged every second or on change
@@ -616,6 +627,7 @@ class SelfdriveD(CruiseHelper):
self.mads.update(CS) self.mads.update(CS)
self.update_alerts(CS) self.update_alerts(CS)
self.button_state_tracker.update(CS)
self.publish_selfdriveState(CS) self.publish_selfdriveState(CS)
self.CS_prev = CS self.CS_prev = CS
@@ -500,7 +500,7 @@ CONFIGS = [
), ),
ProcessConfig( ProcessConfig(
proc_name="dmonitoringd", proc_name="dmonitoringd",
pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"], pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState", "carControl"],
subs=["driverMonitoringState"], subs=["driverMonitoringState"],
ignore=["logMonoTime"], ignore=["logMonoTime"],
should_recv_callback=MessageBasedRcvCallback("driverStateV2"), should_recv_callback=MessageBasedRcvCallback("driverStateV2"),
@@ -511,7 +511,7 @@ CONFIGS = [
pubs=[ pubs=[
"cameraOdometry", "accelerometer", "gyroscope", "liveCalibration", "carState" "cameraOdometry", "accelerometer", "gyroscope", "liveCalibration", "carState"
], ],
subs=["liveLocationKalman", "livePose"], subs=["livePose"],
ignore=["logMonoTime"], ignore=["logMonoTime"],
should_recv_callback=MessageBasedRcvCallback("cameraOdometry"), should_recv_callback=MessageBasedRcvCallback("cameraOdometry"),
tolerance=NUMPY_TOLERANCE, tolerance=NUMPY_TOLERANCE,
@@ -66,7 +66,7 @@ segments = [
# dashcamOnly makes don't need to be tested until a full port is done # dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = ["mock", "body", "psa"] excluded_interfaces = ["mock", "body", "psa"]
BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/" BASE_URL = "https://raw.githubusercontent.com/sunnypilot/ci-artifacts/refs/heads/process-replay/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"} EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
+1
View File
@@ -13,6 +13,7 @@ from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.home import HomeLayoutSP as HomeLayout
class MainState(IntEnum): class MainState(IntEnum):
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
ONROAD_DELAY = 2.5 # seconds ONROAD_DELAY = 2.5 # seconds
@@ -0,0 +1,68 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label
BRAND_FONT_SIZE = 48
BRAND_DESC_SPACING = 12
class HomeLayoutSP(HomeLayout):
def _render_header(self):
font = gui_app.font(FontWeight.MEDIUM)
version_text_width = self.header_rect.width
if self.update_available:
version_text_width -= self.update_notif_rect.width
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
text = tr("UPDATE")
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
if self.alert_count > 0:
version_text_width -= self.alert_notif_rect.width
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
if self.update_available or self.alert_count > 0:
version_text_width -= SPACING * 1.5
version_right = self.header_rect.x + self.header_rect.width
version_left = version_right - version_text_width
brand = "sunnypilot"
description = self.params.get("UpdaterCurrentDescription") or ""
desc_width = 0
if description:
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
desc_width = desc_size.x
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
spacing = BRAND_DESC_SPACING if description else 0
brand_x = version_right - desc_width - spacing - brand_size.x
brand_rect = rl.Rectangle(max(version_left, brand_x), self.header_rect.y, brand_size.x, self.header_rect.height)
gui_label(brand_rect, brand, BRAND_FONT_SIZE, rl.WHITE, font_weight=FontWeight.AUDIOWIDE)
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
self._done_callback = done_callback self._done_callback = done_callback
self._step = 0 self._step = 0
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)) self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
self._content = [ self._content = [
{ {
@@ -9,7 +9,7 @@ from enum import IntEnum
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
@@ -61,10 +61,26 @@ class DisplayLayout(Widget):
f"{value} s" if value < 60 else f"{int(value/60)} m"), f"{value} s" if value < 60 else f"{int(value/60)} m"),
inline=True inline=True
) )
self._screensaver_toggle = toggle_item_sp(
param="ScreenSaverEnabled",
title=lambda: tr("Screen Saver"),
description=lambda: tr("Show a screen saver when the device is offroad and idle, instead of turning the screen off."),
)
self._screensaver_timeout = option_item_sp(
param="ScreenSaverTimeout",
title=lambda: tr("Screen Saver Duration"),
description=lambda: tr("How long the screen saver runs before the screen turns off."),
min_value=60,
max_value=600,
value_change_step=60,
label_callback=lambda value: f"{int(value/60)} m"
)
items = [ items = [
self._onroad_brightness, self._onroad_brightness,
self._onroad_brightness_timer, self._onroad_brightness_timer,
self._interactivity_timeout, self._interactivity_timeout,
self._screensaver_toggle,
self._screensaver_timeout,
] ]
return items return items
@@ -87,6 +103,8 @@ class DisplayLayout(Widget):
brightness_val = self._onroad_brightness.action_item.current_value brightness_val = self._onroad_brightness.action_item.current_value
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)) self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state())
def _render(self, rect): def _render(self, rect):
self._scroller.render(rect) self._scroller.render(rect)
@@ -43,7 +43,7 @@ class ModelsLayout(Widget):
self._initialize_items() self._initialize_items()
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]: for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay"), (self.camera_offset, "CameraOffset")]:
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100)) ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
self._scroller = Scroller(self.items, line_separator=True, spacing=0) self._scroller = Scroller(self.items, line_separator=True, spacing=0)
@@ -93,9 +93,14 @@ class ModelsLayout(Widget):
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle") self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
lambda v: f"{v / 100:.2f} m")
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label, self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle, self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item,
self.lane_turn_value_control, self.lagd_toggle, self.delay_control] self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
def _update_lagd_description(self, lagd_toggle: bool): def _update_lagd_description(self, lagd_toggle: bool):
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " + desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
@@ -232,6 +237,7 @@ class ModelsLayout(Widget):
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls") advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire") turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
live_delay: bool = ui_state.params.get_bool("LagdToggle") live_delay: bool = ui_state.params.get_bool("LagdToggle")
camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None
self.lane_turn_desire_toggle.action_item.set_state(turn_desire) self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls) self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
@@ -240,6 +246,7 @@ class ModelsLayout(Widget):
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100 new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step: if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
self.lane_turn_value_control.action_item.value_change_step = new_step self.lane_turn_value_control.action_item.value_change_step = new_step
self.camera_offset.set_visible(camera_offset)
self._update_lagd_description(live_delay) self._update_lagd_description(live_delay)
self.model_manager = ui_state.sm["modelManagerSP"] self.model_manager = ui_state.sm["modelManagerSP"]
@@ -139,7 +139,8 @@ class SteeringLayout(Widget):
self._nnlc_toggle.action_item.set_state(False) self._nnlc_toggle.action_item.set_state(False)
enforce_torque_enabled = False enforce_torque_enabled = False
nnlc_enabled = False nnlc_enabled = False
self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled) jerk_aware_enabled = ui_state.params.get_bool("LateralJerkTorqueController")
self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled and not jerk_aware_enabled)
self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled) self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled)
self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state()) self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state())
@@ -51,11 +51,18 @@ class LaneChangeSettingsLayout(Widget):
description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " + description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " +
"(BSM) detects a obstructing vehicle, ensuring safe maneuvering."), "(BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
) )
self._road_edge_block = toggle_item_sp(
param="RoadEdgeLaneChangeEnabled",
title=lambda: tr("Block Lane Change: Road Edge Detection"),
description=lambda: tr("Blocks the lane change if the model sees a road edge on your signaled side."),
)
items = [ items = [
self._lane_change_timer, self._lane_change_timer,
LineSeparatorSP(40), LineSeparatorSP(40),
self._bsm_delay, self._bsm_delay,
LineSeparatorSP(40),
self._road_edge_block,
] ]
return items return items
@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
from collections.abc import Callable from collections.abc import Callable
import pyray as rl import pyray as rl
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.multilang import tr, tr_noop
@@ -96,7 +96,10 @@ class MadsSettingsLayout(Widget):
if brand == "rivian": if brand == "rivian":
return True return True
elif brand == "tesla": elif brand == "tesla":
return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS) if ui_state.CP_SP is None or not ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
return True
screen_button = int(ui_state.params.get("TeslaMadsScreenButton", return_default=True))
return screen_button == MadsScreenButtonType.OFF
return False return False
def _update_steering_mode_description(self, button_index: int): def _update_steering_mode_description(self, button_index: int):
@@ -40,6 +40,13 @@ class TorqueSettingsLayout(Widget):
self.cached_torque_versions = json.load(f) self.cached_torque_versions = json.load(f)
def _initialize_items(self): def _initialize_items(self):
self._jerk_aware_toggle = toggle_item_sp(
param="LateralJerkTorqueController",
title=lambda: tr("Lateral Jerk Torque Controller"),
description=lambda: tr("Looks ahead at planned steering to reduce sudden corrections, so the wheel moves " +
"more smoothly through turns. Works with Self-Tune and custom tuning. " +
"Thanks to @twilsonco for the implementation."),
)
self._torque_control_versions = ListItemSP( self._torque_control_versions = ListItemSP(
title=tr("Torque Control Tune Version"), title=tr("Torque Control Tune Version"),
description="Select the version of Torque Control Tune to use.", description="Select the version of Torque Control Tune to use.",
@@ -95,6 +102,7 @@ class TorqueSettingsLayout(Widget):
) )
items = [ items = [
self._jerk_aware_toggle,
self._torque_control_versions, self._torque_control_versions,
self._self_tune_toggle, self._self_tune_toggle,
self._relaxed_tune_toggle, self._relaxed_tune_toggle,
@@ -107,6 +115,8 @@ class TorqueSettingsLayout(Widget):
def _update_state(self): def _update_state(self):
super()._update_state() super()._update_state()
nnlc_enabled = ui_state.params.get_bool("NeuralNetworkLateralControl")
self._jerk_aware_toggle.action_item.set_enabled(ui_state.is_offroad() and not nnlc_enabled)
if not ui_state.params.get_bool("LiveTorqueParamsToggle"): if not ui_state.params.get_bool("LiveTorqueParamsToggle"):
ui_state.params.remove("LiveTorqueParamsRelaxedToggle") ui_state.params.remove("LiveTorqueParamsRelaxedToggle")
self._relaxed_tune_toggle.action_item.set_state(False) self._relaxed_tune_toggle.action_item.set_state(False)
@@ -4,10 +4,11 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp
COOP_STEERING_MIN_KMH = 23 COOP_STEERING_MIN_KMH = 23
OEM_STEERING_MIN_KMH = 48 OEM_STEERING_MIN_KMH = 48
@@ -18,7 +19,14 @@ class TeslaSettings(BrandSettings):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering") self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
self.items = [self.coop_steering_toggle] self.mads_screen_button = multiple_button_item_sp(
title=lambda: tr("MADS Screen Activation"),
description="",
buttons=[lambda: tr("Off"), lambda: tr("3-Finger"), lambda: tr("4-Finger"), lambda: tr("5-Finger")],
param="TeslaMadsScreenButton",
inline=False,
)
self.items = [self.coop_steering_toggle, self.mads_screen_button]
def update_settings(self): def update_settings(self):
is_metric = ui_state.is_metric is_metric = ui_state.is_metric
@@ -41,3 +49,18 @@ class TeslaSettings(BrandSettings):
self.coop_steering_toggle.set_description(coop_steering_desc) self.coop_steering_toggle.set_description(coop_steering_desc)
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad()) self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
has_vehicle_bus = ui_state.CP_SP is not None and bool(ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
self.mads_screen_button.set_visible(has_vehicle_bus)
mads_screen_button_desc = (
f"{tr('Use a multi-finger press on the infotainment screen to toggle MADS.')} " +
f"{tr('This allows the use of full MADS functionality when enabled.')}<br><br>" +
f"{tr('Selecting a higher finger count may reduce accidental activations.')}<br><br>" +
f"<b>{tr('Note: Setting this to Off will reset your MADS settings to default.')}</b>"
)
if not ui_state.is_offroad():
mads_screen_button_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to change.")
mads_screen_button_desc = f"<b>{mads_screen_button_disabled_msg}</b><br><br>{mads_screen_button_desc}"
self.mads_screen_button.set_description(mads_screen_button_desc)
self.mads_screen_button.action_item.set_enabled(ui_state.is_offroad())
@@ -0,0 +1,15 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.widgets.label import UnifiedLabel
class MiciHomeLayoutSP(MiciHomeLayout):
def __init__(self):
super().__init__()
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
@@ -4,7 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
from collections.abc import Callable
import pyray as rl import pyray as rl
from openpilot.cereal import custom from openpilot.cereal import custom
@@ -48,10 +47,8 @@ class CurrentModelInfo(Widget):
self.info_text.render() self.info_text.render()
class ModelsLayoutMici(NavScroller): class ModelsLayoutMici(NavScroller):
def __init__(self, back_callback: Callable): def __init__(self):
super().__init__() super().__init__()
self.set_back_callback(back_callback)
self.original_back_callback = back_callback
self.focused_widget = None self.focused_widget = None
self.current_model_info = CurrentModelInfo() self.current_model_info = CurrentModelInfo()
@@ -85,12 +82,10 @@ class ModelsLayoutMici(NavScroller):
return folders return folders
def _show_selection_view(self, items, back_callback: Callable): def _push_selection_view(self, items):
self._scroller._items = items scroller = NavScroller()
for item in items: scroller._scroller.add_widgets(items)
item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled) gui_app.push_widget(scroller)
self._scroller.scroll_panel.set_offset(0)
self.set_back_callback(back_callback)
def _show_folders(self): def _show_folders(self):
self.focused_widget = self.select_model_btn self.focused_widget = self.select_model_btn
@@ -112,15 +107,18 @@ class ModelsLayoutMici(NavScroller):
folder_buttons.insert(0, btn) folder_buttons.insert(0, btn)
else: else:
folder_buttons.append(btn) folder_buttons.append(btn)
self._show_selection_view(folder_buttons, self._reset_main_view) self._push_selection_view(folder_buttons)
def _pop_to_main(self):
gui_app.pop_widgets_to(self)
def _select_model(self, bundle): def _select_model(self, bundle):
ui_state.params.put("ModelManager_DownloadIndex", bundle.index) ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
self._reset_main_view() self._pop_to_main()
def _select_default(self): def _select_default(self):
ui_state.params.remove("ModelManager_ActiveBundle") ui_state.params.remove("ModelManager_ActiveBundle")
self._reset_main_view() self._pop_to_main()
def _select_folder(self, folder_name): def _select_folder(self, folder_name):
favs = ui_state.params.get("ModelManager_Favs") favs = ui_state.params.get("ModelManager_Favs")
@@ -135,13 +133,7 @@ class ModelsLayoutMici(NavScroller):
btn = BigButton(txt) btn = BigButton(txt)
btn.set_click_callback(lambda b=bundle: self._select_model(b)) btn.set_click_callback(lambda b=bundle: self._select_model(b))
btns.append(btn) btns.append(btn)
self._show_selection_view(btns, self._show_folders) self._push_selection_view(btns)
def _reset_main_view(self):
self._scroller._items = self.main_items # type: ignore[assignment] # ty: ignore[invalid-assignment]
self.set_back_callback(self.original_back_callback)
self._scroller.scroll_panel.set_offset(0)
self._scroller.scroll_to(0)
def hide_event(self): def hide_event(self):
super().hide_event() super().hide_event()
@@ -32,11 +32,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
BIG_ICON_SIZE) BIG_ICON_SIZE)
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE) self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget) sunnylink_panel = SunnylinkLayoutMici()
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55)) sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel)) sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget) models_panel = ModelsLayoutMici()
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE)) models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel)) models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
""" """
import pyray as rl import pyray as rl
from collections.abc import Callable
from openpilot.cereal import custom from openpilot.cereal import custom
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
@@ -54,9 +53,8 @@ class SunnylinkInfo(Widget):
self.sponsor_text.render() self.sponsor_text.render()
class SunnylinkLayoutMici(NavScroller): class SunnylinkLayoutMici(NavScroller):
def __init__(self, back_callback: Callable): def __init__(self):
super().__init__() super().__init__()
self.set_back_callback(back_callback)
self._restore_in_progress = False self._restore_in_progress = False
self._backup_in_progress = False self._backup_in_progress = False
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled") self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
@@ -12,6 +12,7 @@ from openpilot.common.params import Params
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP
OpenpilotState = log.SelfdriveState.OpenpilotState OpenpilotState = log.SelfdriveState.OpenpilotState
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
@@ -38,6 +39,9 @@ class UIStateSP:
self.sunnylink_state = SunnylinkState() self.sunnylink_state = SunnylinkState()
self.screensaver = ScreenSaverSP(params=self.params)
self.screensaver_enabled: bool = False
self.active_bundle = None self.active_bundle = None
self.blindspot: bool = False self.blindspot: bool = False
self.chevron_metrics = None self.chevron_metrics = None
@@ -170,6 +174,7 @@ class UIStateSP:
self.turn_signals = self.params.get_bool("ShowTurnSignals") self.turn_signals = self.params.get_bool("ShowTurnSignals")
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True) self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
self.always_offroad = self.params.get_bool("OffroadMode") self.always_offroad = self.params.get_bool("OffroadMode")
self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled")
if not self._sp_initialized: if not self._sp_initialized:
self._sp_initialized = True self._sp_initialized = True
@@ -184,10 +189,15 @@ class UIStateSP:
self.params.put_bool("EnforceTorqueControl", False, block=True) self.params.put_bool("EnforceTorqueControl", False, block=True)
self.params.put_bool("NeuralNetworkLateralControl", False, block=True) self.params.put_bool("NeuralNetworkLateralControl", False, block=True)
if self.params.get_bool("LateralJerkTorqueController") and self.params.get_bool("NeuralNetworkLateralControl"):
self.params.put_bool("LateralJerkTorqueController", False, block=True)
self.params.put_bool("NeuralNetworkLateralControl", False, block=True)
# Angle steering: no torque-based lateral controls # Angle steering: no torque-based lateral controls
if CP.steerControlType == car.CarParams.SteerControlType.angle: if CP.steerControlType == car.CarParams.SteerControlType.angle:
self.params.remove("EnforceTorqueControl") self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl") self.params.remove("NeuralNetworkLateralControl")
self.params.remove("LateralJerkTorqueController")
# Alpha longitudinal: clear if not available # Alpha longitudinal: clear if not available
if not CP.alphaLongitudinalAvailable: if not CP.alphaLongitudinalAvailable:
@@ -200,6 +210,7 @@ class UIStateSP:
# No CarParams: clear all car-dependent params as safety default # No CarParams: clear all car-dependent params as safety default
self.params.remove("EnforceTorqueControl") self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl") self.params.remove("NeuralNetworkLateralControl")
self.params.remove("LateralJerkTorqueController")
self.params.remove("AlphaLongitudinalEnabled") self.params.remove("AlphaLongitudinalEnabled")
# No longitudinal control: no experimental mode or DEC # No longitudinal control: no experimental mode or DEC
@@ -224,10 +235,26 @@ class UIStateSP:
class DeviceSP: class DeviceSP:
def __init__(self):
self._blocked_by_screensaver: bool = False
def _set_awake(self, on: bool, _ui_state=None): def _set_awake(self, on: bool, _ui_state=None):
self._blocked_by_screensaver = False
if _ui_state.boot_offroad_mode == 1 and not on: if _ui_state.boot_offroad_mode == 1 and not on:
_ui_state.params.put_bool("OffroadMode", True) _ui_state.params.put_bool("OffroadMode", True)
if not on and _ui_state.screensaver_enabled:
if _ui_state.screensaver.was_dismissed:
if gui_app.get_active_widget() == _ui_state.screensaver:
gui_app.pop_widget()
elif _ui_state.screensaver.is_active:
self._blocked_by_screensaver = True
else:
_ui_state.screensaver.initialize()
gui_app.push_widget(_ui_state.screensaver)
self._blocked_by_screensaver = True
@staticmethod @staticmethod
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
if not awake or not _ui_state.started: if not awake or not _ui_state.started:
@@ -338,8 +338,11 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
settings_cases: Cases = [ settings_cases: Cases = [
lambda: scroll_through_cases(toggle_cases), lambda: scroll_through_cases(toggle_cases),
None, # sunnylink (just open and close)
None, # models (just open and close)
lambda: scroll_through_cases(network_cases), lambda: scroll_through_cases(network_cases),
lambda: scroll_through_cases(device_cases), lambda: scroll_through_cases(device_cases),
lambda: script.wait(WAIT_SHORT), # software
lambda: script.wait(WAIT_SHORT), # pairing lambda: script.wait(WAIT_SHORT), # pairing
lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up) lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up)
lambda: scroll_through_cases(developer_cases), lambda: scroll_through_cases(developer_cases),
+2
View File
@@ -340,6 +340,8 @@ class Device(DeviceSP):
def _set_awake(self, on: bool, _ui_state=None): def _set_awake(self, on: bool, _ui_state=None):
if on != self._awake: if on != self._awake:
super()._set_awake(on, _ui_state or ui_state) super()._set_awake(on, _ui_state or ui_state)
if self._blocked_by_screensaver:
return
self._awake = on self._awake = on
cloudlog.debug(f"setting display power {int(on)}") cloudlog.debug(f"setting display power {int(on)}")
HARDWARE.set_display_power(on) HARDWARE.set_display_power(on)
+8 -5
View File
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
from opendbc.car import structs from opendbc.car import structs
from opendbc.safety import ALTERNATIVE_EXPERIENCE from opendbc.safety import ALTERNATIVE_EXPERIENCE
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla") MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla")
@@ -21,17 +21,20 @@ class MadsSteeringModeOnBrake:
DISENGAGE = 2 DISENGAGE = 2
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP) -> bool: def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params) -> bool:
if CP.brand == 'rivian': if CP.brand == 'rivian':
return True return True
if CP.brand == 'tesla': if CP.brand == 'tesla':
return not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS if not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
return True
screen_button = int(params.get("TeslaMadsScreenButton", return_default=True))
return screen_button == MadsScreenButtonType.OFF
return False return False
def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params):
if get_mads_limited_brands(CP, CP_SP): if get_mads_limited_brands(CP, CP_SP, params):
return MadsSteeringModeOnBrake.DISENGAGE return MadsSteeringModeOnBrake.DISENGAGE
return params.get("MadsSteeringMode", return_default=True) return params.get("MadsSteeringMode", return_default=True)
@@ -63,7 +66,7 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p
# MADS is currently partially supported for these platforms due to lack of consistent states to engage controls # MADS is currently partially supported for these platforms due to lack of consistent states to engage controls
# Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms # Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms
# TODO-SP: To enable MADS full support for Rivian and most Tesla, identify consistent signals for MADS toggling # TODO-SP: To enable MADS full support for Rivian and most Tesla, identify consistent signals for MADS toggling
mads_partial_support = get_mads_limited_brands(CP, CP_SP) mads_partial_support = get_mads_limited_brands(CP, CP_SP, params)
if mads_partial_support: if mads_partial_support:
params.put("MadsSteeringMode", 2, block=True) params.put("MadsSteeringMode", 2, block=True)
params.put_bool("MadsUnifiedEngagementMode", True, block=True) params.put_bool("MadsUnifiedEngagementMode", True, block=True)
@@ -13,7 +13,7 @@ from openpilot.selfdrive.selfdrived.events import Events
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
EventName = log.OnroadEvent.EventName EventName = log.OnroadEvent.EventName
@@ -38,6 +38,12 @@ def make_panda_state(mocker, controls_allowed_lateral=True):
return ps return ps
def make_params_mock(mocker, values):
params = mocker.MagicMock()
params.get = mocker.MagicMock(side_effect=lambda k, **kwargs: values[k])
return params
def make_mads(mocker, steering_mode): def make_mads(mocker, steering_mode):
sd = mocker.MagicMock() sd = mocker.MagicMock()
sd.CP = structs.CarParams() sd.CP = structs.CarParams()
@@ -223,15 +229,27 @@ class TestBrandSteeringModeRestrictions:
params = mocker.MagicMock() params = mocker.MagicMock()
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
def test_tesla_with_vehicle_bus_uses_param(self, mocker): @pytest.mark.parametrize("screen_button", [MadsScreenButtonType.THREE_FINGER,
MadsScreenButtonType.FOUR_FINGER,
MadsScreenButtonType.FIVE_FINGER])
def test_tesla_with_vehicle_bus_uses_param(self, mocker, screen_button):
CP = structs.CarParams() CP = structs.CarParams()
CP.brand = "tesla" CP.brand = "tesla"
CP_SP = structs.CarParamsSP() CP_SP = structs.CarParamsSP()
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
params = mocker.MagicMock() params = make_params_mock(mocker, {"TeslaMadsScreenButton": screen_button,
params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE) "MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE
def test_tesla_with_vehicle_bus_screen_button_off_forced_to_disengage(self, mocker):
CP = structs.CarParams()
CP.brand = "tesla"
CP_SP = structs.CarParamsSP()
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
params = make_params_mock(mocker, {"TeslaMadsScreenButton": MadsScreenButtonType.OFF,
"MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
@pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"]) @pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"])
def test_other_brands_use_param(self, mocker, brand): def test_other_brands_use_param(self, mocker, brand):
CP = structs.CarParams() CP = structs.CarParams()
-15
View File
@@ -82,18 +82,3 @@ if os.path.isfile(supercombo_onnx):
compile_combined('supercombo', compile_combined('supercombo',
f'--supercombo-onnx {supercombo_onnx}', f'--supercombo-onnx {supercombo_onnx}',
'driving_combined_supercombo_tinygrad.pkl') 'driving_combined_supercombo_tinygrad.pkl')
if PC:
inputs = tinygrad_files + [File(Dir("#openpilot/sunnypilot/modeld_v2").File("install_models_pc.py").abspath)]
outputs = []
model_dir = Dir("models").abspath
cmd = f'python3 {Dir("#openpilot/sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
if File(f"models/{model_name}.onnx").exists():
inputs.append(File(f"models/{model_name}.onnx"))
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
outputs.append(File(f"models/{model_name}_metadata.pkl"))
if outputs:
lenv.Command(outputs, inputs, cmd)
+268 -384
View File
@@ -10,471 +10,355 @@ import argparse
import os import os
import pickle import pickle
import time import time
from functools import partial
from collections import defaultdict from collections import defaultdict
from functools import partial
import numpy as np import numpy as np
from tinygrad.tensor import Tensor os.environ['GMMU'] = '0'
def _patch_tinygrad_fetch_fw():
import hashlib
import pathlib
import zstandard
from tinygrad import helpers
_orig_fetch_fw = 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_fetch_fw(path, name, sha256)
helpers.fetch_fw = fetch_fw
_patch_tinygrad_fetch_fw()
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, make_frame_prepare, sample_desire, sample_skip, shift_and_sample
from tinygrad import dtypes
from tinygrad.device import Device from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit from tinygrad.engine.jit import TinyJit
from tinygrad.tensor import Tensor
from openpilot.selfdrive.modeld.compile_modeld import (
NV12Frame, make_frame_prepare,
shift_and_sample, sample_skip, sample_desire,
)
MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy') MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy')
def _detect_desire_key(policy_input_shapes): def _detect_desire_key(shapes: dict) -> str | None:
for k in policy_input_shapes: return next((key for key in shapes if key.startswith('desire')), None)
if k.startswith('desire'):
return k
return None
def _detect_vision_keys(vision_input_shapes): def _detect_vision_keys(shapes: dict) -> tuple[str | None, str | None]:
img_keys = sorted([k for k in vision_input_shapes if 'img' in k]) img_keys = sorted(key for key in shapes if 'img' in key)
road_key = next((k for k in img_keys if 'big' not in k), None) return (
wide_key = next((k for k in img_keys if 'big' in k), None) next((key for key in img_keys if 'big' not in key), None),
if road_key is None or wide_key is None: next((key for key in img_keys if 'big' in key), None)
raise ValueError(f"Cannot determine road/wide image keys from {list(vision_input_shapes.keys())}") )
return road_key, wide_key
def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device): def derive_frame_skip(vision_input_shapes: dict, policy_input_shapes: dict) -> int:
road_key, _ = _detect_vision_keys(vision_input_shapes) features_buffer = policy_input_shapes.get('features_buffer')
img = vision_input_shapes[road_key] return 1 if not features_buffer or features_buffer[1] >= 99 else 4
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
fb = policy_input_shapes['features_buffer']
desire_key = _detect_desire_key(policy_input_shapes)
dp = policy_input_shapes[desire_key]
tc = policy_input_shapes.get('traffic_convention', (1, 2))
npy = {
'desire': np.zeros(dp[2], dtype=np.float32),
'traffic_convention': np.zeros(tc, dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
handled = {'features_buffer', desire_key, 'traffic_convention'}
for key, shape in policy_input_shapes.items():
if key in handled:
continue
npy[key] = np.zeros(shape, 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(),
'feat_q': Tensor(np.zeros((frame_skip * (fb[1] - 1) + 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(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def make_run_split_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h, def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tuple[dict, list[int]]:
vision_features_slice, frame_skip, desire_key, extra_policy_keys, desire_key = _detect_desire_key(input_shapes)
vision_road_key, vision_wide_key, prepare_only=False): shapes = {}
frame_prepare = make_frame_prepare(nv12, model_w, model_h) if desire_key:
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) shapes['desire'] = (input_shapes[desire_key][2],)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
def run_policy(img_q, big_img_q, feat_q, desire_q, desire, traffic_convention, tfm, big_tfm, frame, big_frame, **extra): if is_supercombo and 'features_buffer' in input_shapes:
npy_tensors = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT), fb = input_shapes['features_buffer']
desire.to(Device.DEFAULT), traffic_convention.to(Device.DEFAULT)] shapes['prev_feat'] = (fb[0], fb[2])
extra_device = {k: extra[k].to(Device.DEFAULT) for k in extra_policy_keys}
Tensor.realize(*npy_tensors, *extra_device.values())
tfm, big_tfm, desire, traffic_convention = npy_tensors
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn) for key, shape in input_shapes.items():
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn) if key not in (desire_key, 'features_buffer') and 'img' not in key:
shapes[key] = tuple(shape)
if prepare_only: sizes = [int(np.prod(size)) for size in shapes.values()]
return img, big_img return shapes, sizes
vision_out = next(iter(vision_runner({vision_road_key: img, vision_wide_key: big_img}).values())).cast('float32')
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
inputs = {'features_buffer': feat_buf, desire_key: desire_buf, 'traffic_convention': traffic_convention, **extra_device}
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
return vision_out, policy_out
return run_policy
def compile_split_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip, def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT,
vision_runner, policy_runner, vision_metadata, policy_metadata): is_supercombo: bool = False, use_packed: bool = True) -> tuple[dict, dict]:
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...") road_key, _ = _detect_vision_keys(input_shapes)
if not road_key:
vision_features_slice = vision_metadata['output_slices']['hidden_state'] raise ValueError("Vision road key missing from input shapes.")
vision_input_shapes = vision_metadata['input_shapes']
policy_input_shapes = policy_metadata['input_shapes']
desire_key = _detect_desire_key(policy_input_shapes)
extra_policy_keys = [k for k in policy_input_shapes if k not in ('features_buffer', desire_key, 'traffic_convention')]
vision_road_key, vision_wide_key = _detect_vision_keys(vision_input_shapes)
_run = make_run_split_policy(vision_runner, policy_runner, nv12, model_w, model_h,
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
vision_road_key, vision_wide_key, prepare_only)
run_policy_jit = TinyJit(_run, prune=True)
SEED = 42
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, 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):
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
for v in npy.values():
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
Device.default.synchronize()
st = time.perf_counter()
outs = fn(**input_queues, frame=frame, big_frame=big_frame)
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 fn, val, buffers
print('capture + replay')
run_policy_jit, test_val, test_buffers = random_inputs_run_fn(run_policy_jit, SEED)
print('pickle round trip')
run_policy_jit = pickle.loads(pickle.dumps(run_policy_jit))
random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers, expect_match=True)
random_inputs_run_fn(run_policy_jit, SEED+1, test_val, test_buffers, expect_match=False)
return run_policy_jit
def derive_frame_skip(vision_input_shapes, policy_input_shapes):
fb = policy_input_shapes.get('features_buffer')
if fb is None:
return 1
fb_history = fb[1]
if fb_history >= 99:
return 1
return 4
def make_supercombo_input_queues(input_shapes, frame_skip, device):
img_shape = input_shapes.get('img', input_shapes.get('input_imgs'))
if img_shape is None:
raise ValueError("No img input found in model shapes")
img_shape = input_shapes[road_key]
n_frames = img_shape[1] // 6 n_frames = img_shape[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3]) img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
numpy_keys = {} desire_key = _detect_desire_key(input_shapes)
queue_keys = {} if not desire_key:
raise ValueError("Desire key missing from input shapes.")
for key, shape in input_shapes.items(): desire_shape = input_shapes[desire_key]
if 'img' in key: features_buffer = input_shapes.get('features_buffer')
continue
if len(shape) == 3 and shape[1] > 1:
if key.startswith('desire'):
numpy_keys[key] = np.zeros(shape[2], dtype=np.float32)
queue_keys[f'{key}_q'] = Tensor(
np.zeros((frame_skip * shape[1], shape[0], shape[2]), dtype=np.float32),
device=device).contiguous().realize()
elif key == 'features_buffer':
queue_keys['feat_q'] = Tensor(
np.zeros((frame_skip * (shape[1] - 1) + 1, shape[0], shape[2]), dtype=np.float32),
device=device).contiguous().realize()
else:
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
elif len(shape) == 2:
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
if 'traffic_convention' not in numpy_keys: if use_packed: # remove packed detection block after all models are recompiled
tc_shape = input_shapes.get('traffic_convention', (1, 2)) npy_arrays = {
numpy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32) 'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32)
}
numpy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32) shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
numpy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32) packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
input_queues = { split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else []
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else []
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), for (k, s), v in zip(shapes.items(), split_views, strict=True):
**queue_keys, npy_arrays[k] = v.reshape(s)
**{k: Tensor(v, device='NPY').realize() for k, v in numpy_keys.items()},
} queues = {
return input_queues, numpy_keys '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(),
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
}
if features_buffer:
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
dtype=np.float32), device=device).contiguous().realize()
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
else:
# TODO-SP: Remove legacy queuing fallback else block after all models are recompiled
npy_arrays = {
'desire': np.zeros(desire_shape[2], dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32)
}
for key, shape in input_shapes.items():
if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key):
npy_arrays[key] = np.zeros(shape, dtype=np.float32)
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(),
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
dtype=np.float32), device=device).contiguous().realize()
}
if features_buffer:
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
dtype=np.float32), device=device).contiguous().realize()
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()})
return queues, npy_arrays
def make_run_supercombo(model_runner, nv12: NV12Frame, model_w, model_h, def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict,
features_slice, frame_skip, input_shapes, prepare_only=False): frame_skip: int, device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]:
frame_prepare = make_frame_prepare(nv12, model_w, model_h) return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False, use_packed=use_packed)
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int,
device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]:
return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True, use_packed=use_packed)
def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, model_size: tuple[int, int],
features_slice: slice, frame_skip: int, input_shapes: dict, prepare_only: bool):
frame_prepare = make_frame_prepare(nv12, *model_size)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
desire_key = _detect_desire_key(input_shapes) desire_key = _detect_desire_key(input_shapes)
if desire_key is None: road_key, wide_key = _detect_vision_keys(input_shapes)
raise ValueError(f"No desire* key found in input_shapes: {list(input_shapes.keys())}")
road_img_key, wide_img_key = _detect_vision_keys(input_shapes)
extra_policy_keys = [k for k in input_shapes
if k not in (desire_key, 'features_buffer', 'traffic_convention')
and 'img' not in k]
def run_supercombo(img_q, big_img_q, feat_q, desire_q, if not desire_key or not road_key or not wide_key:
frame, big_frame, **kwargs): raise ValueError("Missing required vision or desire keys in input shapes.")
desire = kwargs.get(desire_key)
traffic_convention = kwargs.get('traffic_convention')
tfm = kwargs['tfm']
big_tfm = kwargs['big_tfm']
tfm = tfm.to(Device.DEFAULT) is_supercombo = vision_runner is None
big_tfm = big_tfm.to(Device.DEFAULT) npy_shapes, npy_sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
desire = desire.to(Device.DEFAULT)
traffic_convention = traffic_convention.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, desire, traffic_convention)
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn) def runner(img_q, big_img_q, feat_q, packed_npy_inputs, frame, big_frame, tfm, big_tfm, **kwargs):
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn) desire_q = kwargs['desire_q']
packed_npy_inputs_dev = packed_npy_inputs.to(Device.DEFAULT)
tfm_dev = tfm.to(Device.DEFAULT)
big_tfm_dev = big_tfm.to(Device.DEFAULT)
Tensor.realize(packed_npy_inputs_dev, tfm_dev, big_tfm_dev)
img = shift_and_sample(img_q, frame_prepare(frame, tfm_dev).unsqueeze(0), sample_skip_fn).realize()
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm_dev).unsqueeze(0), sample_skip_fn).realize()
if prepare_only: if prepare_only:
return img, big_img return img, big_img
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn) unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)]
feat_buf = sample_skip_fn(feat_q) unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True))
inputs = {road_img_key: img, wide_img_key: big_img, desire_dev = unpacked_dict['desire']
desire_key: desire_buf, 'features_buffer': feat_buf, desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn).realize()
'traffic_convention': traffic_convention}
for k in extra_policy_keys:
if k in kwargs:
inputs[k] = kwargs[k].to(Device.DEFAULT)
model_out = next(iter(model_runner(inputs).values())).cast('float32') inputs = {desire_key: desire_buf}
for key, tensor_val in unpacked_dict.items():
if key not in ('desire', 'prev_feat'):
inputs[key] = tensor_val
new_feat = model_out[:, features_slice].reshape(1, -1).unsqueeze(0) if 'prev_feat' in unpacked_dict:
shift_and_sample(feat_q, new_feat, sample_skip_fn) prev_feat_dev = unpacked_dict['prev_feat']
inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).realize()
return model_out if vision_runner:
vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize()
if 'features_buffer' not in inputs:
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32').realize() for pol_runner in policy_runners]
return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0])
return run_supercombo inputs.update({road_key: img, wide_key: big_img})
if 'features_buffer' not in inputs:
inputs['features_buffer'] = sample_skip_fn(feat_q)
policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize()
if 'features_buffer' not in inputs and features_slice is not None:
new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0)
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
return policy_out
return runner
def make_run_vision_multi_policy(vision_runner, policy_runners, nv12: NV12Frame, model_w, model_h, def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_only: bool, frame_skip: int, vision_runner, policy_runners: list, metadata: dict):
vision_features_slice, frame_skip, desire_key, extra_policy_keys, print(f"Compiling combined JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
vision_road_key, vision_wide_key, prepare_only=False):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
def run_multi_policy(img_q, big_img_q, feat_q, desire_q, desire, all_shapes = {key: value for meta in metadata.values() for key, value in meta['input_shapes'].items()}
traffic_convention, tfm, big_tfm, frame, big_frame, **extra):
npy_tensors = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT),
desire.to(Device.DEFAULT), traffic_convention.to(Device.DEFAULT)]
extra_device = {k: extra[k].to(Device.DEFAULT) for k in extra_policy_keys}
Tensor.realize(*npy_tensors, *extra_device.values())
tfm, big_tfm, desire, traffic_convention = npy_tensors
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn) feat_meta = metadata.get('vision') or metadata.get('model') or metadata.get('policy')
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn) if not feat_meta:
raise ValueError("Could not find vision, model, or policy metadata.")
if prepare_only: features_slice = feat_meta['output_slices']['hidden_state']
return img, big_img WARP_DEV = 'CPU' if "USBGPU" in os.environ else Device.DEFAULT
vision_out = next(iter(vision_runner({vision_road_key: img, vision_wide_key: big_img}).values())).cast('float32') is_supercombo = vision_runner is None
run_func = create_jit_runner(vision_runner, policy_runners, nv12, model_size, features_slice, frame_skip, all_shapes, prepare_only)
run_jit = TinyJit(run_func, prune=True)
queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT, is_supercombo=is_supercombo)
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
inputs = {'features_buffer': feat_buf, desire_key: desire_buf, 'traffic_convention': traffic_convention, **extra_device}
policy_outputs = []
for runner in policy_runners:
policy_out = next(iter(runner(inputs).values())).cast('float32')
policy_outputs.append(policy_out)
return (vision_out, *policy_outputs)
return run_multi_policy
def _warmup_and_serialize(run_jit, input_queues, npy, nv12):
for i in range(3): for i in range(3):
rng = np.random.default_rng(42 + i) rng = np.random.default_rng(42 + i)
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize() frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize() big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
for v in npy.values(): for arr in npy_arrays.values():
v[:] = rng.standard_normal(v.shape).astype(v.dtype) arr[:] = rng.standard_normal(arr.shape).astype(arr.dtype)
Device.default.synchronize() Device.default.synchronize()
st = time.perf_counter() start_time = time.perf_counter()
run_jit(**input_queues, frame=frame, big_frame=big_frame) run_jit(**queues, frame=frame, big_frame=big_frame)
mt = time.perf_counter() mid_time = time.perf_counter()
Device.default.synchronize() Device.default.synchronize()
et = time.perf_counter() print(f" [{i + 1}/3] enqueue {(mid_time - start_time) * 1e3:6.2f} ms -- total {(time.perf_counter() - start_time) * 1e3:6.2f} ms")
print(f" [{i + 1}/3] enqueue {(mt - st) * 1e3:6.2f} ms -- total {(et - st) * 1e3:6.2f} ms")
return pickle.loads(pickle.dumps(run_jit)) # TODO-SP: switch to dump_oob/load_oob on next full recompile of all models
return pickle.loads(pickle.dumps(run_jit)) if not prepare_only else run_jit
def compile_supercombo(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip, def _parse_size(size_str: str) -> tuple[int, int]:
model_runner, metadata): width, height = size_str.lower().split('x')
print(f"Compiling combined supercombo JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...") return int(width), int(height)
features_slice = metadata['output_slices']['hidden_state']
input_shapes = metadata['input_shapes']
_run = make_run_supercombo(model_runner, nv12, model_w, model_h,
features_slice, frame_skip, input_shapes, prepare_only)
run_jit = TinyJit(_run, prune=True)
input_queues, npy = make_supercombo_input_queues(input_shapes, frame_skip, Device.DEFAULT)
run_jit = _warmup_and_serialize(run_jit, input_queues, npy, nv12)
return run_jit
def compile_multi_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip, def read_file_chunked_to_shm(path):
vision_runner, policy_runners, vision_metadata, policy_metadata): if not path:
print(f"Compiling combined multi-policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...") return None
import atexit
vision_features_slice = vision_metadata['output_slices']['hidden_state'] import shutil
vision_input_shapes = vision_metadata['input_shapes'] from openpilot.common.file_chunker import open_file_chunked
policy_input_shapes = policy_metadata['input_shapes'] from openpilot.common.hardware.hw import Paths
desire_key = _detect_desire_key(policy_input_shapes) shm_path = os.path.join(Paths.shm_path(), os.path.basename(path))
extra_policy_keys = [k for k in policy_input_shapes if k not in ('features_buffer', desire_key, 'traffic_convention')] atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path))
vision_road_key, vision_wide_key = _detect_vision_keys(vision_input_shapes) with open(shm_path, 'wb') as dst, open_file_chunked(path) as src:
shutil.copyfileobj(src, dst)
_run = make_run_vision_multi_policy(vision_runner, policy_runners, nv12, model_w, model_h, return shm_path
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
vision_road_key, vision_wide_key, prepare_only)
run_jit = TinyJit(_run, prune=True)
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
run_jit = _warmup_and_serialize(run_jit, input_queues, npy, nv12)
return run_jit
def _parse_size(s): def _compile_for_resolutions(camera_resolutions: list, model_size: tuple[int, int], frame_skip: int,
w, h = s.lower().split('x') vision_runner, policy_runners: list, metadata: dict) -> dict:
return int(w), int(h) from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
return {
(cam_w, cam_h): {
name: compile_and_warmup(NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)), model_size, prepare_only,
frame_skip, vision_runner, policy_runners, metadata)
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
for cam_w, cam_h in camera_resolutions
}
def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
runners, keys = [], []
for name, onnx_arg in [('policy', args.policy_onnx), ('off_policy', args.off_policy_onnx), ('on_policy', args.on_policy_onnx)]:
if onnx_arg:
runners.append(OnnxRunner(onnx_arg))
keys.append(name)
return runners, keys
if __name__ == "__main__": 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 from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
from tinygrad.nn.onnx import OnnxRunner
p = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2") parser = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2")
p.add_argument('--model-type', choices=MODEL_TYPES, required=True) parser.add_argument('--model-type', choices=MODEL_TYPES, required=True)
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH') parser.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True) parser.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True)
p.add_argument('--frame-skip', type=int, default=None, help='frame skip value (auto-derived if not provided)') parser.add_argument('--frame-skip', type=int, default=None, help='frame skip value (auto-derived if not provided)')
p.add_argument('--output', required=True) parser.add_argument('--output', required=True)
p.add_argument('--vision-onnx', help='vision ONNX (for split models)') parser.add_argument('--vision-onnx', help='vision ONNX (for split models)')
p.add_argument('--policy-onnx', help='policy ONNX (for vision_policy)') parser.add_argument('--policy-onnx', help='policy ONNX (for vision_policy)')
p.add_argument('--off-policy-onnx', help='off-policy ONNX (for vision_multi_policy)') parser.add_argument('--off-policy-onnx', help='off-policy ONNX (for vision_multi_policy)')
p.add_argument('--on-policy-onnx', help='on-policy ONNX (for vision_multi_policy)') parser.add_argument('--on-policy-onnx', help='on-policy ONNX (for vision_multi_policy)')
p.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)') parser.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)')
args = p.parse_args() args = parser.parse_args()
out = defaultdict(dict) output_data = defaultdict(dict)
args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx)
args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx)
args.off_policy_onnx = read_file_chunked_to_shm(args.off_policy_onnx)
args.on_policy_onnx = read_file_chunked_to_shm(args.on_policy_onnx)
args.supercombo_onnx = read_file_chunked_to_shm(args.supercombo_onnx)
vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None
if args.model_type == 'vision_policy': if args.model_type == 'vision_policy':
assert args.vision_onnx and args.policy_onnx assert vision_runner and args.policy_onnx
vision_runner = OnnxRunner(args.vision_onnx) policy_runners = [OnnxRunner(args.policy_onnx)]
policy_runner = OnnxRunner(args.policy_onnx) output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx), 'policy': make_metadata_dict(args.policy_onnx)}
out['metadata']['vision'] = make_metadata_dict(args.vision_onnx)
out['metadata']['policy'] = make_metadata_dict(args.policy_onnx)
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip(out['metadata']['vision']['input_shapes'],
out['metadata']['policy']['input_shapes'])
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
model_w, model_h = args.model_size
out[(cam_w, cam_h)] = {
name: compile_split_policy(nv12, model_w, model_h, prepare_only, frame_skip,
vision_runner, policy_runner,
out['metadata']['vision'], out['metadata']['policy'])
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
elif args.model_type == 'supercombo': elif args.model_type == 'supercombo':
assert args.supercombo_onnx assert args.supercombo_onnx
model_runner = OnnxRunner(args.supercombo_onnx) policy_runners = [OnnxRunner(args.supercombo_onnx)]
out['metadata']['model'] = make_metadata_dict(args.supercombo_onnx) output_data['metadata'] = {'model': make_metadata_dict(args.supercombo_onnx)}
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip({}, out['metadata']['model']['input_shapes'])
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
model_w, model_h = args.model_size
out[(cam_w, cam_h)] = {
name: compile_supercombo(nv12, model_w, model_h, prepare_only, frame_skip,
model_runner, out['metadata']['model'])
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
elif args.model_type == 'vision_multi_policy': elif args.model_type == 'vision_multi_policy':
assert args.vision_onnx assert vision_runner
vision_runner = OnnxRunner(args.vision_onnx) policy_runners, policy_names = _load_policy_runners(args)
out['metadata']['vision'] = make_metadata_dict(args.vision_onnx) output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx)}
for name in policy_names:
runner_arg = getattr(args, f"{name}_onnx")
output_data['metadata'][name] = make_metadata_dict(runner_arg)
policy_runners = [] policy_keys = [key for key in output_data['metadata'].keys() if key != 'vision']
policy_onnxes = [] first_policy_meta = output_data['metadata'][policy_keys[0]] if policy_keys else {}
if args.policy_onnx: vision_meta = output_data['metadata'].get('vision', {})
policy_onnxes.append(('policy', args.policy_onnx))
if args.off_policy_onnx:
policy_onnxes.append(('off_policy', args.off_policy_onnx))
if args.on_policy_onnx:
policy_onnxes.append(('on_policy', args.on_policy_onnx))
for name, onnx_path in policy_onnxes: derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {}))
runner = OnnxRunner(onnx_path) output_data.update(_compile_for_resolutions(args.camera_resolutions, args.model_size, derived_frame_skip,
policy_runners.append(runner) vision_runner, policy_runners, output_data['metadata']))
out['metadata'][name] = make_metadata_dict(onnx_path)
first_policy_key = policy_onnxes[0][0] with open(args.output, "wb") as file:
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip(out['metadata']['vision']['input_shapes'], # TODO-SP: switch to dump_oob from openpilot/selfdrive/helpers on next full recompile of all models
out['metadata'][first_policy_key]['input_shapes']) pickle.dump(output_data, file)
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
model_w, model_h = args.model_size
out[(cam_w, cam_h)] = {
name: compile_multi_policy(nv12, model_w, model_h, prepare_only, frame_skip,
vision_runner, policy_runners,
out['metadata']['vision'], out['metadata'][first_policy_key])
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
with open(args.output, "wb") as f:
pickle.dump(out, f)
pkl_size = os.path.getsize(args.output) pkl_size = os.path.getsize(args.output)
print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)") print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)")
from openpilot.common.file_chunker import chunk_file, get_chunk_targets from openpilot.common.file_chunker import chunk_file, get_chunk_targets
chunk_targets = get_chunk_targets(args.output, pkl_size) chunk_targets = get_chunk_targets(args.output, pkl_size)
chunk_file(args.output, chunk_targets) chunk_file(args.output, chunk_targets)
num_chunks = len(chunk_targets) - 1 print(f"Chunked into {len(chunk_targets) - 1} file(s)")
print(f"Chunked into {num_chunks} file(s)")
@@ -1,75 +0,0 @@
#!/usr/bin/env python3
import sys
import shutil
import pickle
import codecs
from pathlib import Path
from openpilot.common.hardware.hw import Paths
from openpilot.sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name
def generate_metadata_pkl(model_path, output_path):
try:
model = MetadataOnnxPBParser(model_path).parse()
output_slices = get_metadata_value_by_name(model, 'output_slices')
if not output_slices:
return False
metadata = {
'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"]),
}
with open(output_path, 'wb') as f:
pickle.dump(metadata, f)
return True
except Exception:
return False
def install_models(model_dir):
model_dir = Path(model_dir)
models = ["driving_off_policy", "driving_on_policy", "driving_vision"]
found_models = []
for model in models:
if (model_dir / f"{model}.onnx").exists():
found_models.append(model)
if not found_models:
return
try:
custom_name = input(f"Found models ({', '.join(found_models)}). Enter model short name (e.g. wmiv4): ").strip()
except EOFError:
return
if not custom_name:
print("No name provided, skipping installation.")
return
dest_dir = Path(Paths.model_root())
dest_dir.mkdir(parents=True, exist_ok=True)
for model in found_models:
onnx_path = model_dir / f"{model}.onnx"
tinygrad_pkl = model_dir / f"{model}_tinygrad.pkl"
metadata_pkl = model_dir / f"{model}_metadata.pkl"
if not metadata_pkl.exists():
generate_metadata_pkl(onnx_path, metadata_pkl)
dest_tinygrad = dest_dir / f"{model}_{custom_name}_tinygrad.pkl"
dest_metadata = dest_dir / f"{model}_{custom_name}_metadata.pkl"
if tinygrad_pkl.exists():
shutil.move(str(tinygrad_pkl), str(dest_tinygrad))
if metadata_pkl.exists():
shutil.move(str(metadata_pkl), str(dest_metadata))
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: install_models_pc.py <model_dir>")
sys.exit(1)
install_models(sys.argv[1])
+87 -34
View File
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
""" """
import os import os
os.environ['GMMU'] = '0'
from openpilot.common.hardware import TICI from openpilot.common.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU' os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
USBGPU = "USBGPU" in os.environ USBGPU = "USBGPU" in os.environ
@@ -23,6 +24,11 @@ from setproctitle import setproctitle
from openpilot.cereal.messaging import PubMaster, SubMaster from openpilot.cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params from opendbc.car.car_helpers import get_demo_car_params
from tinygrad.tensor import Tensor
from tinygrad.device import Device
from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
@@ -30,6 +36,7 @@ from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix from openpilot.common.transformations.model import get_warp_matrix
from openpilot.system import sentry from openpilot.system import sentry
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
@@ -37,10 +44,12 @@ from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_p
from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.constants import Plan
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad" PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad"
@@ -99,29 +108,37 @@ class ModelState(ModelStateBase):
self._init_combined(pkl_path, cam_w, cam_h, model_bundle) self._init_combined(pkl_path, cam_w, cam_h, model_bundle)
def _init_combined(self, pkl_path, cam_w, cam_h, bundle): def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
from tinygrad.tensor import Tensor
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
from tinygrad.device import Device
from openpilot.common.file_chunker import open_file_chunked
cloudlog.warning(f"loading combined pkl: {pkl_path}") cloudlog.warning(f"loading combined pkl: {pkl_path}")
# TODO-SP: switch to load_oob from openpilot/selfdrive/helpers on next full recompile of all models
jits = pickle.load(open_file_chunked(pkl_path)) jits = pickle.load(open_file_chunked(pkl_path))
self.DEV = Device.DEFAULT self.DEV = Device.DEFAULT
self.WARP_DEV = 'CPU' if USBGPU else self.DEV
self.QUEUE_DEV = self.DEV
metadata = jits['metadata'] metadata = jits['metadata']
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
# TODO-SP: Remove legacy use_packed detection block after all models are recompiled
captured = getattr(self._run_policy, 'captured', None)
if captured is not None:
use_packed = 'packed_npy_inputs' in getattr(captured, 'expected_names', [])
else:
use_packed = True
if 'model' in metadata: if 'model' in metadata:
model_metadata = metadata['model'] model_metadata = metadata['model']
self.vision_output_slices = model_metadata['output_slices'] self.vision_output_slices = model_metadata['output_slices']
self.policy_output_slices = {} self.policy_output_slices = {}
self._policy_slices_list = [] self._policy_slices_list = []
self._combined_model_type = 'supercombo' self._combined_model_type = 'supercombo'
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k] self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key]
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
frame_skip = derive_frame_skip({}, model_metadata['input_shapes']) frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV) self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'],
frame_skip, device=self.QUEUE_DEV, use_packed=use_packed)
else: else:
vision_metadata = metadata['vision'] vision_metadata = metadata['vision']
policy_keys = [k for k in metadata if k != 'vision'] policy_keys = [k for k in metadata if k != 'vision']
@@ -139,11 +156,12 @@ class ModelState(ModelStateBase):
policy_input_shapes = first_policy_metadata['input_shapes'] policy_input_shapes = first_policy_metadata['input_shapes']
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k] self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes) frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV) self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes,
frame_skip, device=self.QUEUE_DEV, use_packed=use_packed)
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire'))
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser self._road_key = next(key for key in self._vision_input_names if 'big' not in key)
self.parser = SplitParser() if self._combined_model_type != 'supercombo' else CombinedParser() self._wide_key = next(key for key in self._vision_input_names if 'big' in key)
is_20hz = bundle.is20hz if bundle else self._combined_model_type in ('split', 'multi_policy') is_20hz = bundle.is20hz if bundle else self._combined_model_type in ('split', 'multi_policy')
if is_20hz: if is_20hz:
@@ -153,20 +171,24 @@ class ModelState(ModelStateBase):
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
self.constants = ModelConstants() self.constants = ModelConstants()
if self._combined_model_type != 'supercombo':
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
self.parser = SplitParser()
else:
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
self.parser = CombinedParser()
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32) self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
self.full_frames: dict = {} self.full_frames: dict = {}
self._blob_cache: dict = {} self._blob_cache: dict = {}
nv12_info = get_nv12_info(cam_w, cam_h) nv12_info = get_nv12_info(cam_w, cam_h)
self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info) self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info)
self._run_policy = jits[(cam_w, cam_h)]['run_policy'] yuv_size = self.frame_buf_params[self._road_key][3]
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
road_name = next(k for k in self._vision_input_names if 'big' not in k)
yuv_size = self.frame_buf_params[road_name][3]
self._warp_enqueue( self._warp_enqueue(
**self.input_queues, **self.input_queues,
frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.DEV).contiguous().realize(), frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize(),
big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.DEV).contiguous().realize()) big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize())
@property @property
@@ -179,30 +201,28 @@ class ModelState(ModelStateBase):
@property @property
def desire_key(self) -> str: def desire_key(self) -> str:
return next(k for k in self.numpy_inputs if k.startswith('desire')) return self._desire_key
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
from tinygrad.tensor import Tensor
for key in bufs.keys(): for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3] yuv_size = self.frame_buf_params[key][3]
cache_key = (key, ptr) cache_key = (key, ptr)
if cache_key not in self._blob_cache: if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.DEV) 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] self.full_frames[key] = self._blob_cache[cache_key]
desire_key = self.desire_key desire_key = self.desire_key
inputs[desire_key][0] = 0 inputs[desire_key][0] = 0
self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0) self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
self.prev_desire[:] = inputs[desire_key] self.prev_desire[:] = inputs[desire_key]
for key in ('traffic_convention', 'lateral_control_params'): for key in ('traffic_convention', 'lateral_control_params', 'action_t'):
if key in self.numpy_inputs and key in inputs: if key in self.numpy_inputs and key in inputs:
self.numpy_inputs[key][:] = inputs[key] self.numpy_inputs[key][:] = inputs[key]
road_key = next(n for n in bufs if 'big' not in n) road_key = self._road_key
wide_key = next(n for n in bufs if 'big' in n) wide_key = self._wide_key
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3) self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3) self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
@@ -216,17 +236,26 @@ class ModelState(ModelStateBase):
model_output = raw_outputs.numpy().flatten() model_output = raw_outputs.numpy().flatten()
sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()} sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
outputs = self.parser.parse_outputs(sliced) outputs = self.parser.parse_outputs(sliced)
if 'prev_feat' in self.numpy_inputs:
self.numpy_inputs['prev_feat'][:] = model_output[self.vision_output_slices['hidden_state']]
else: else:
vision_output = raw_outputs[0].numpy().flatten() vision_output = raw_outputs[0].numpy().flatten()
vision_sliced = {k: vision_output[np.newaxis, v] for k, v in self.vision_output_slices.items()} vision_sliced = {k: vision_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
outputs = self.parser.parse_vision_outputs(vision_sliced) outputs = self.parser.parse_vision_outputs(vision_sliced)
if 'prev_feat' in self.numpy_inputs and 'hidden_state' in self.vision_output_slices:
self.numpy_inputs['prev_feat'][:] = vision_output[self.vision_output_slices['hidden_state']]
for i, policy_slices in enumerate(self._policy_slices_list): for i, policy_slices in enumerate(self._policy_slices_list):
policy_output = raw_outputs[i + 1].numpy().flatten() policy_output = raw_outputs[i + 1].numpy().flatten()
policy_sliced = {k: policy_output[np.newaxis, v] for k, v in policy_slices.items()} policy_sliced = {k: policy_output[np.newaxis, v] for k, v in policy_slices.items()}
parsed = self.parser.parse_policy_outputs(policy_sliced) parsed = self.parser.parse_policy_outputs(policy_sliced)
if 'off' in self._policy_keys[i] and self._has_on_policy: if ('off' in self._policy_keys[i]
and self._has_on_policy
and any('plan' in self._policy_slices_list[j] for j, k in enumerate(self._policy_keys) if 'on' in k.lower())):
parsed.pop('plan', None) parsed.pop('plan', None)
outputs.update(parsed) outputs.update(parsed)
if 'planplus' in outputs and 'plan' in outputs: if 'planplus' in outputs and 'plan' in outputs:
@@ -237,17 +266,30 @@ class ModelState(ModelStateBase):
buf[0, :-1] = buf[0, 1:] buf[0, :-1] = buf[0, 1:]
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
# TODO-SP: This is a hack to prevent GPU corruption by calculating in CPU space, it can be removed on next recompile
if 'prev_feat' not in self.numpy_inputs and 'feat_q' in self.input_queues:
feat_val = self.input_queues['feat_q'].numpy()
self.input_queues['feat_q'].assign(feat_val).realize()
return outputs return outputs
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
plan = model_output['plan'][0] if 'action' not in model_output:
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, plan = model_output['plan'][0]
action_t=long_action_t) desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
action_t=long_action_t)
curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0]
if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan)
desired_curvature = get_curvature_from_output(model_output, curvature_plan, v_ego, lat_action_t, self.mlsim)
else:
desired_accel = model_output['action'][0, 1]
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
curvature_plan = plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan
desired_curvature = get_curvature_from_output(model_output, curvature_plan, v_ego, lat_action_t, self.mlsim)
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
if v_ego > self.MIN_LAT_CONTROL_SPEED: if v_ego > self.MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
@@ -326,6 +368,7 @@ def main(demo=False):
DH = DesireHelper() DH = DesireHelper()
meta_constants = load_meta_constants() meta_constants = load_meta_constants()
RELC = RoadEdgeLaneChangeController()
while True: while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame # Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -400,6 +443,12 @@ def main(demo=False):
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names} bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names} transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
frame_delay = DT_MDL # compensate for time passed since the frame was captured: current_time - timestamp_eof is 50ms on average
action_delay = DT_MDL / 2 # middle of the interval between model output (current state) and next frame (expected state)
lat_action_t = lat_delay + frame_delay + action_delay
long_action_t = long_delay + frame_delay + action_delay
inputs:dict[str, np.ndarray] = { inputs:dict[str, np.ndarray] = {
model.desire_key: vec_desire, model.desire_key: vec_desire,
'traffic_convention': traffic_convention, 'traffic_convention': traffic_convention,
@@ -408,6 +457,9 @@ def main(demo=False):
if 'lateral_control_params' in model.numpy_inputs: if 'lateral_control_params' in model.numpy_inputs:
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32) inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
if 'action_t' in model.numpy_inputs:
inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32)
mt1 = time.perf_counter() mt1 = time.perf_counter()
model_output = model.run(bufs, transforms, inputs, prepare_only) model_output = model.run(bufs, transforms, inputs, prepare_only)
mt2 = time.perf_counter() mt2 = time.perf_counter()
@@ -419,7 +471,7 @@ def main(demo=False):
posenet_send = messaging.new_message('cameraOdometry') posenet_send = messaging.new_message('cameraOdometry')
mdv2sp_send = messaging.new_message('modelDataV2SP') mdv2sp_send = messaging.new_message('modelDataV2SP')
action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego) action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego)
prev_action = action prev_action = action
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
@@ -429,7 +481,8 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight] r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
@@ -1,13 +1,16 @@
import numpy as np import numpy as np
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
def safe_exp(x, out=None): def safe_exp(x, out=None):
# -11 is around 10**14, more causes float16 overflow # -11 is around 10**14, more causes float16 overflow
return np.exp(np.clip(x, -np.inf, 11), out=out) return np.exp(np.clip(x, -np.inf, 11), out=out)
def sigmoid(x): def sigmoid(x):
return 1. / (1. + safe_exp(-x)) return 1. / (1. + safe_exp(-x))
def softmax(x, axis=-1): def softmax(x, axis=-1):
x -= np.max(x, axis=axis, keepdims=True) x -= np.max(x, axis=axis, keepdims=True)
if x.dtype == np.float32 or x.dtype == np.float64: if x.dtype == np.float32 or x.dtype == np.float64:
@@ -17,6 +20,19 @@ def softmax(x, axis=-1):
x /= np.sum(x, axis=axis, keepdims=True) x /= np.sum(x, axis=axis, keepdims=True)
return x return x
def _infer_mhp(slice_size: int, prod_out_shape: int, max_in_n: int = 16, max_out_n: int = 6) -> tuple[int, int]:
for out_n in range(max_out_n + 1):
per = 2 * prod_out_shape + out_n
if per <= 0:
continue
if slice_size % per == 0:
in_n = slice_size // per
if 1 <= in_n <= max_in_n:
return in_n, out_n
return 1, 0 # single hypothesis, no weights — matches a non-MDN output
class Parser: class Parser:
def __init__(self, ignore_missing=False): def __init__(self, ignore_missing=False):
self.ignore_missing = ignore_missing self.ignore_missing = ignore_missing
@@ -40,17 +56,22 @@ class Parser:
raw = outs[name] raw = outs[name]
outs[name] = sigmoid(raw) outs[name] = sigmoid(raw)
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): def parse_mdn(self, name, outs, out_shape, in_N=0, out_N=0):
if self.check_missing(outs, name): if self.check_missing(outs, name):
return return
raw = outs[name] raw = outs[name]
raw = raw.reshape((raw.shape[0], max(in_N, 1), -1))
if in_N == 0 and out_N == 0:
prod = int(np.prod(out_shape))
in_N, out_N = _infer_mhp(raw.shape[1], prod)
raw = raw.reshape((raw.shape[0], in_N, -1))
n_values = (raw.shape[2] - out_N)//2 n_values = (raw.shape[2] - out_N)//2
pred_mu = raw[:,:,:n_values] pred_mu = raw[:,:,:n_values]
pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) pred_std = safe_exp(raw[:,:,n_values: 2*n_values])
if in_N > 1: if in_N > 1 and out_N > 0:
weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype)
for i in range(out_N): for i in range(out_N):
weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1)
@@ -61,7 +82,6 @@ class Parser:
weights[fidx] = weights[fidx][idxs] weights[fidx] = weights[fidx][idxs]
pred_mu[fidx] = pred_mu[fidx][idxs] pred_mu[fidx] = pred_mu[fidx][idxs]
pred_std[fidx] = pred_std[fidx][idxs] pred_std[fidx] = pred_std[fidx][idxs]
assert out_shape is not None
full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
outs[name + '_weights'] = weights outs[name + '_weights'] = weights
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
@@ -74,37 +94,43 @@ class Parser:
idxs = np.argsort(weights[fidx,:,hidx])[::-1] idxs = np.argsort(weights[fidx,:,hidx])[::-1]
pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]]
pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]]
elif in_N > 1 and out_N == 0:
# MHP without weights: keep every hypothesis intact, surface them as
# ``*_hypotheses`` and propagate the full multi-hypothesis tensor.
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape)
pred_mu_final = pred_mu
pred_std_final = pred_std
else: else:
pred_mu_final = pred_mu pred_mu_final = pred_mu
pred_std_final = pred_std pred_std_final = pred_std
if out_N > 1: if out_N > 1 or (in_N > 1 and out_N == 0):
assert out_shape is not None n_selections = out_N if out_N > 1 else in_N
final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) final_shape = tuple([raw.shape[0], n_selections] + list(out_shape))
else: else:
assert out_shape is not None
final_shape = tuple([raw.shape[0],] + list(out_shape)) final_shape = tuple([raw.shape[0],] + list(out_shape))
outs[name] = pred_mu_final.reshape(final_shape) outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape)
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, # supercombo (4955 / 102) and newer variants (e.g. 990 / 144).
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) self.parse_mdn('plan', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) self.parse_mdn('lane_lines', outs, out_shape=(ModelConstants.NUM_LANE_LINES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) self.parse_mdn('road_edges', outs, out_shape=(ModelConstants.NUM_ROAD_EDGES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('pose', outs, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('road_transform', outs, out_shape=(ModelConstants.POSE_WIDTH,))
if 'sim_pose' in outs: if 'sim_pose' in outs:
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('sim_pose', outs, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, self.parse_mdn('lead', outs, out_shape=(ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH))
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
if 'lat_planner_solution' in outs: if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) self.parse_mdn('lat_planner_solution', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs: if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) self.parse_mdn('desired_curvature', outs, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob', 'meta']: for k in ['lead_prob', 'lane_lines_prob', 'meta']:
self.parse_binary_crossentropy(k, outs) self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN, ModelConstants.DESIRE_PRED_WIDTH))
return outs return outs
@@ -123,7 +123,7 @@ class Parser:
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,))
if 'lane_lines' in outs: if 'lane_lines' in outs:
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, self.parse_mdn('lane_lines', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
if 'lane_lines_prob' in outs: if 'lane_lines_prob' in outs:
self.parse_binary_crossentropy('lane_lines_prob', outs) self.parse_binary_crossentropy('lane_lines_prob', outs)
if 'lead_prob' in outs: if 'lead_prob' in outs:
@@ -134,9 +134,11 @@ class Parser:
self.parse_binary_crossentropy('meta', outs) self.parse_binary_crossentropy('meta', outs)
if 'road_edges' in outs: if 'road_edges' in outs:
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, self.parse_mdn('road_edges', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
if 'sim_pose' in outs: if 'sim_pose' in outs:
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
if 'action' in outs:
self.parse_mdn('action', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.ACTION_WIDTH,))
def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
@@ -67,22 +67,12 @@ class TestStockEquivalence:
state = model_state_factory(ARCHETYPES['vision_policy_split']) state = model_state_factory(ARCHETYPES['vision_policy_split'])
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES) frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
# action_t is a deep-model prerequisite the SP loader doesn't provide yet; see skip_keys below
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)} stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY') stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
# TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue) assert set(state.input_queues.keys()) == set(stock_queues.keys())
# prev_feat is a stock QCOM corruption workaround handled inside the SP loader's JIT path assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
skip_keys = {'action_t', 'prev_feat'} assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
# stock packs the per-key policy inputs into packed_npy_inputs; the npy views carry the individual keys
stock_queue_keys = set(stock_queues.keys())
if 'packed_npy_inputs' in stock_queue_keys:
stock_queue_keys.remove('packed_npy_inputs')
stock_queue_keys |= set(stock_npy.keys())
assert set(state.input_queues.keys()) == stock_queue_keys - skip_keys, \
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={stock_queue_keys}"
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
def test_split_queue_keys_work_with_desire_key(self, model_state_factory): def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
@@ -1,103 +0,0 @@
import os
os.environ['DEV'] = 'CPU'
import pytest
import numpy as np
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
('img', 'big_img'),
('input_imgs', 'big_input_imgs'),
]
class MockVisionBuf:
def __init__(self, w, h):
self.width = w
self.height = h
_, _, _, yuv_size = get_nv12_info(w, h)
self.data = np.zeros(yuv_size, dtype=np.uint8)
@pytest.mark.parametrize("buffer_length", [2, 5])
def test_warp_initialization(buffer_length):
warp = Warp(buffer_length)
assert warp.buffer_length == buffer_length
assert warp.img_buffer_shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
@pytest.mark.parametrize("buffer_length", [2, 5])
@pytest.mark.parametrize("cam_w, cam_h", CAMERA_CONFIGS)
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
def test_warp_process(buffer_length, cam_w, cam_h, road, wide):
warp = Warp(buffer_length)
mock_buf = MockVisionBuf(cam_w, cam_h)
transform = np.eye(3, dtype=np.float32).flatten()
bufs = {road: mock_buf, wide: mock_buf}
transforms = {road: transform, wide: transform}
out = warp.process(bufs, transforms)
assert isinstance(out, dict)
assert road in out and wide in out
assert out[road].shape == (1, 12, MODEL_H // 2, MODEL_W // 2)
assert out[wide].shape == (1, 12, MODEL_H // 2, MODEL_W // 2)
key = (cam_w, cam_h)
assert key in warp.jit_cache
out2 = warp.process(bufs, transforms)
assert out2[road].shape == out[road].shape
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
def test_warp_buffer_shift(road, wide):
warp = Warp(2)
cam_w, cam_h = CAMERA_CONFIGS[1]
transform = np.eye(3, dtype=np.float32).flatten()
buf1 = MockVisionBuf(cam_w, cam_h)
buf1.data[0] = 255
bufs1 = {road: buf1, wide: buf1}
transforms = {road: transform, wide: transform}
out1 = warp.process(bufs1, transforms)
road1 = out1[road].numpy().copy()
buf2 = MockVisionBuf(cam_w, cam_h)
buf2.data[0] = 128
bufs2 = {road: buf2, wide: buf2}
out2 = warp.process(bufs2, transforms)
assert not np.array_equal(road1, out2[road].numpy())
@pytest.mark.parametrize("buffer_length", [2, 5])
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
def test_warp_buffer_accumulation(buffer_length, road, wide):
warp = Warp(buffer_length)
cam_w, cam_h = CAMERA_CONFIGS[0]
transform = np.eye(3, dtype=np.float32).flatten()
transforms = {road: transform, wide: transform}
outputs = []
for i in range(buffer_length + 1):
buf = MockVisionBuf(cam_w, cam_h)
buf.data[:] = i * 10
out = warp.process({road: buf, wide: buf}, transforms)
outputs.append(out[road].numpy().copy())
assert warp.full_buffers['img'].shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
for i in range(1, len(outputs)):
assert not np.array_equal(outputs[i - 1], outputs[i])
def test_warp_different_cameras_same_instance():
warp = Warp(2)
transform = np.eye(3, dtype=np.float32).flatten()
buf1 = MockVisionBuf(*CAMERA_CONFIGS[0])
warp.process({'img': buf1, 'big_img': buf1}, {'img': transform, 'big_img': transform})
assert len(warp.jit_cache) == 1
buf2 = MockVisionBuf(*CAMERA_CONFIGS[1])
warp.process({'img': buf2, 'big_img': buf2}, {'img': transform, 'big_img': transform})
assert len(warp.jit_cache) == 2
-171
View File
@@ -1,171 +0,0 @@
import pickle
import time
import numpy as np
from pathlib import Path
from tinygrad.tensor import Tensor
from tinygrad.engine.jit import TinyJit
from tinygrad.device import Device
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, make_frame_prepare as _make_frame_prepare
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height),
(_os_fisheye.width, _os_fisheye.height),
]
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
return _make_frame_prepare(nv12, model_w, model_h)
def warp_pkl_path(w, h):
from openpilot.selfdrive.modeld.helpers import MODELS_DIR
return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
def make_update_img_input(frame_prepare, model_w, model_h):
def update_img_input_tinygrad(tensor, frame, M_inv):
M_inv = M_inv.to(Device.DEFAULT)
new_img = frame_prepare(frame, M_inv)
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
return update_img_input_tinygrad
def make_update_both_imgs(frame_prepare, model_w, model_h):
update_img = make_update_img_input(frame_prepare, model_w, model_h)
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
calib_big_img_buffer, new_big_img, M_inv_big):
calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
return calib_img_pair, calib_big_img_pair
return update_both_imgs_tinygrad
MODELS_DIR = Path(__file__).parent / 'models'
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
UPSTREAM_BUFFER_LENGTH = 5
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
def compile_v2_warp(cam_w, cam_h, buffer_length):
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
update_img_jit = TinyJit(update_both_imgs, prune=True)
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
big_full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
new_frame_np = np.random.default_rng(0).integers(0, 256, yuv_size, dtype=np.uint8)
new_big_frame_np = np.random.default_rng(1).integers(0, 256, yuv_size, dtype=np.uint8)
for i in range(10):
img_inputs = [full_buffer,
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
big_img_inputs = [big_full_buffer,
Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
inputs = img_inputs + big_img_inputs
Device.default.synchronize()
st = time.perf_counter()
_ = update_img_jit(*inputs)
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")
pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length)
with open(pkl_path, "wb") as f:
pickle.dump(update_img_jit, f)
print(f" Saved to {pkl_path}")
jit = pickle.load(open(pkl_path, "rb"))
verify_frame = np.random.default_rng(0).integers(0, 256, yuv_size, dtype=np.uint8)
verify_big_frame = np.random.default_rng(1).integers(0, 256, yuv_size, dtype=np.uint8)
fresh_inputs = [
Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize(),
Tensor.from_blob(verify_frame.ctypes.data, (yuv_size,), dtype='uint8').realize(),
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY'),
Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize(),
Tensor.from_blob(verify_big_frame.ctypes.data, (yuv_size,), dtype='uint8').realize(),
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY'),
]
jit(*fresh_inputs)
class Warp:
def __init__(self, buffer_length=2):
self.buffer_length = buffer_length
self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
self.jit_cache = {}
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
self._blob_cache: dict[int, Tensor] = {}
self._nv12_cache: dict[tuple[int, int], int] = {}
self.transforms_np = {k: np.zeros((3, 3), dtype=np.float32) for k in ['img', 'big_img']}
self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()}
def process(self, bufs, transforms):
if not bufs:
return {}
road = next(n for n in bufs if 'big' not in n)
wide = next(n for n in bufs if 'big' in n)
cam_w, cam_h = bufs[road].width, bufs[road].height
key = (cam_w, cam_h)
if key not in self.jit_cache:
v2_pkl = v2_warp_pkl_path(cam_w, cam_h, self.buffer_length)
if v2_pkl.exists():
with open(v2_pkl, 'rb') as f:
self.jit_cache[key] = pickle.load(f)
elif self.buffer_length == UPSTREAM_BUFFER_LENGTH:
upstream_pkl = warp_pkl_path(cam_w, cam_h)
if upstream_pkl.exists():
with open(upstream_pkl, 'rb') as f:
self.jit_cache[key] = pickle.load(f)
if key not in self.jit_cache:
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
if key not in self._nv12_cache:
self._nv12_cache[key] = get_nv12_info(cam_w, cam_h)[3]
yuv_size = self._nv12_cache[key]
road_ptr = bufs[road].data.ctypes.data
wide_ptr = bufs[wide].data.ctypes.data
if road_ptr not in self._blob_cache:
self._blob_cache[road_ptr] = Tensor.from_blob(road_ptr, (yuv_size,), dtype='uint8')
if wide_ptr not in self._blob_cache:
self._blob_cache[wide_ptr] = Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
road_blob = self._blob_cache[road_ptr]
wide_blob = self._blob_cache[wide_ptr] if wide_ptr != road_ptr else Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
np.copyto(self.transforms_np['img'], transforms[road].reshape(3, 3))
np.copyto(self.transforms_np['big_img'], transforms[wide].reshape(3, 3))
Device.default.synchronize()
res = self.jit_cache[key](
self.full_buffers['img'], road_blob, self.transforms['img'],
self.full_buffers['big_img'], wide_blob, self.transforms['big_img'],
)
out_road = res[0].realize()
out_wide = res[1].realize()
return {road: out_road, wide: out_wide}
if __name__ == "__main__":
for cam_w, cam_h in CAMERA_CONFIGS:
for bl in [2, 5]:
compile_v2_warp(cam_w, cam_h, bl)
+29 -5
View File
@@ -6,11 +6,12 @@ See the LICENSE.md file in the root directory for more details.
""" """
import time import time
import os
import requests import requests
from requests.exceptions import (SSLError, RequestException, HTTPError) from requests.exceptions import (SSLError, RequestException, HTTPError)
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.hardware.hw import Paths
from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible
from openpilot.cereal import custom from openpilot.cereal import custom
@@ -26,11 +27,35 @@ class ModelParser:
download_uri.sha256 = download_uri_data.get("sha256") download_uri.sha256 = download_uri_data.get("sha256")
return download_uri return download_uri
@staticmethod
def _parse_chunk(chunk_data) -> custom.ModelManagerSP.Chunk:
chunk = custom.ModelManagerSP.Chunk()
chunk.fileName = chunk_data.get("file_name")
chunk.sha256 = chunk_data.get("sha256")
return chunk
@staticmethod @staticmethod
def _parse_artifact(artifact_data) -> custom.ModelManagerSP.Artifact: def _parse_artifact(artifact_data) -> custom.ModelManagerSP.Artifact:
artifact = custom.ModelManagerSP.Artifact() artifact = custom.ModelManagerSP.Artifact()
artifact.fileName = artifact_data.get("file_name") artifact.fileName = artifact_data.get("file_name")
artifact.downloadUri = ModelParser._parse_download_uri(artifact_data.get("download_uri", {})) artifact.downloadUri = ModelParser._parse_download_uri(artifact_data.get("download_uri", {}))
if "chunks" in artifact_data:
artifact.chunks = [ModelParser._parse_chunk(chunk_data) for chunk_data in artifact_data["chunks"]]
try:
model_dir = Paths.model_root()
os.makedirs(model_dir, exist_ok=True)
manifest_path = os.path.join(model_dir, f"{artifact.fileName}.chunkmanifest")
num_chunks = str(len(artifact.chunks))
if not os.path.exists(manifest_path) or open(manifest_path).read().strip() != num_chunks:
with open(manifest_path, "w") as f:
f.write(num_chunks)
cloudlog.info(f"Wrote chunk manifest for {artifact.fileName}: {num_chunks} chunks")
except Exception as e:
cloudlog.warning(f"Failed to write chunk manifest for {artifact.fileName}: {e}")
return artifact return artifact
@staticmethod @staticmethod
@@ -39,8 +64,6 @@ class ModelParser:
model.type = model_data.get("type") model.type = model_data.get("type")
model.artifact = ModelParser._parse_artifact(model_data.get("artifact", {})) model.artifact = ModelParser._parse_artifact(model_data.get("artifact", {}))
if metadata := model_data.get("metadata"):
model.metadata = ModelParser._parse_artifact(metadata)
return model return model
@staticmethod @staticmethod
@@ -116,7 +139,7 @@ class ModelCache:
class ModelFetcher: class ModelFetcher:
"""Handles fetching and caching of model data from remote source""" """Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v17.json" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
def __init__(self, params: Params): def __init__(self, params: Params):
self.params = params self.params = params
@@ -184,4 +207,5 @@ if __name__ == "__main__":
# Print artifact details # Print artifact details
print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}") print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}")
# Print metadata details # Print metadata details
print(f"Metadata: {model.metadata.fileName}, Download URI: {model.metadata.downloadUri.uri}") if model.artifact.chunks:
print(f"Contains {len(model.artifact.chunks)} chunks.")
+15 -8
View File
@@ -18,7 +18,7 @@ from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRai
from openpilot.common.hardware.hw import Paths from openpilot.common.hardware.hw import Paths
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 15 REQUIRED_JSON_VERSION = 16
CUSTOM_MODEL_PATH = Paths.model_root() CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
@@ -56,12 +56,20 @@ def is_bundle_version_compatible(bundle: dict) -> bool:
def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]: def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]:
artifacts = [] artifacts = []
from openpilot.common.file_chunker import get_chunk_name
for model in getattr(bundle, 'models', []) or []: for model in getattr(bundle, 'models', []) or []:
for artifact in (getattr(model, 'artifact', None), getattr(model, 'metadata', None)): for artifact in (getattr(model, 'artifact', None),):
if artifact and getattr(artifact, 'fileName', None) and getattr(artifact, 'downloadUri', None): if artifact and getattr(artifact, 'fileName', None):
sha256 = getattr(artifact.downloadUri, 'sha256', None) if len(artifact.chunks) > 0:
if sha256: for i, chunk in enumerate(artifact.chunks):
artifacts.append((artifact.fileName, sha256)) chunk_name = get_chunk_name(artifact.fileName, i, len(artifact.chunks))
if getattr(chunk, 'sha256', None):
artifacts.append((chunk_name, chunk.sha256))
else:
if getattr(artifact, 'downloadUri', None):
sha256 = getattr(artifact.downloadUri, 'sha256', None)
if sha256:
artifacts.append((artifact.fileName, sha256))
return artifacts return artifacts
@@ -156,8 +164,7 @@ def _get_model():
def load_metadata(): def load_metadata():
model = _get_model() metadata_path = METADATA_PATH
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" if model else METADATA_PATH
with open(metadata_path, 'rb') as f: with open(metadata_path, 'rb') as f:
return pickle.load(f) return pickle.load(f)
+51 -41
View File
@@ -38,11 +38,11 @@ class ModelManagerSP:
if not self.selected_bundle: if not self.selected_bundle:
return return
for model in self.selected_bundle.models: for model in self.selected_bundle.models:
for artifact in (model.artifact, model.metadata): artifact = model.artifact
if artifact is not source_artifact and artifact.fileName == source_artifact.fileName: if artifact is not source_artifact and artifact.fileName == source_artifact.fileName:
artifact.downloadProgress.status = source_artifact.downloadProgress.status artifact.downloadProgress.status = source_artifact.downloadProgress.status
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
def _calculate_eta(self, filename: str, progress: float) -> int: def _calculate_eta(self, filename: str, progress: float) -> int:
"""Calculate ETA based on elapsed time and current progress""" """Calculate ETA based on elapsed time and current progress"""
@@ -89,20 +89,16 @@ class ModelManagerSP:
del self._download_start_times[model.fileName] del self._download_start_times[model.fileName]
async def _download_chunked(self, base_url: str, base_path: str, artifact) -> None: async def _download_chunked(self, base_url: str, base_path: str, artifact) -> None:
from openpilot.common.file_chunker import get_manifest_path, get_chunk_name from openpilot.common.file_chunker import get_chunk_name, get_manifest_path
manifest_url = get_manifest_path(base_url)
num_chunks = len(artifact.chunks)
if num_chunks == 0:
raise ValueError("No chunks defined in artifact")
manifest_path = get_manifest_path(base_path) manifest_path = get_manifest_path(base_path)
async with aiohttp.ClientSession() as session:
async with session.get(manifest_url) as resp:
if resp.status == 404:
raise FileNotFoundError
resp.raise_for_status()
num_chunks = int((await resp.read()).strip())
self._download_start_times[artifact.fileName] = time.monotonic() self._download_start_times[artifact.fileName] = time.monotonic()
for i in range(num_chunks): for i, _ in enumerate(artifact.chunks):
chunk_url = get_chunk_name(base_url, i, num_chunks) chunk_url = get_chunk_name(base_url, i, num_chunks)
chunk_path = get_chunk_name(base_path, i, num_chunks) chunk_path = get_chunk_name(base_path, i, num_chunks)
chunk_downloaded = 0 chunk_downloaded = 0
@@ -117,7 +113,7 @@ class ModelManagerSP:
if self.params.get("ModelManager_DownloadIndex") is None: if self.params.get("ModelManager_DownloadIndex") is None:
raise Exception("Download cancelled") raise Exception("Download cancelled")
intra = chunk_downloaded / max(chunk_size, 1) intra = chunk_downloaded / max(chunk_size, 1)
progress = min(99, (i + intra) / num_chunks * 100) progress = min(99.0, ((i + intra) / num_chunks) * 100)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
artifact.downloadProgress.progress = progress artifact.downloadProgress.progress = progress
artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress) artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress)
@@ -140,7 +136,22 @@ class ModelManagerSP:
full_path = os.path.join(destination_path, filename) full_path = os.path.join(destination_path, filename)
try: try:
if await verify_file(full_path, expected_hash): is_cached = False
if len(artifact.chunks) > 0:
from openpilot.common.file_chunker import get_chunk_name
chunks_valid = True
for i, chunk in enumerate(artifact.chunks):
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
if not await verify_file(chunk_path, chunk.sha256):
chunks_valid = False
break
if chunks_valid and len(artifact.chunks) > 0:
is_cached = True
else:
if await verify_file(full_path, expected_hash):
is_cached = True
if is_cached:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100 artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0 artifact.downloadProgress.eta = 0
@@ -148,13 +159,17 @@ class ModelManagerSP:
self._report_status() self._report_status()
return return
try: if len(artifact.chunks) > 0:
await self._download_chunked(url, full_path, artifact) await self._download_chunked(url, full_path, artifact)
except (FileNotFoundError, aiohttp.ClientResponseError): from openpilot.common.file_chunker import get_chunk_name
for i, chunk in enumerate(artifact.chunks):
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
if not await verify_file(chunk_path, chunk.sha256):
raise ValueError(f"Hash validation failed for chunk {i+1} of {filename}")
else:
await self._download_file(url, full_path, artifact) await self._download_file(url, full_path, artifact)
if not await verify_file(full_path, expected_hash):
if not await verify_file(full_path, expected_hash): raise ValueError(f"Hash validation failed for {filename}")
raise ValueError(f"Hash validation failed for {filename}")
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
artifact.downloadProgress.progress = 100 artifact.downloadProgress.progress = 100
@@ -170,18 +185,15 @@ class ModelManagerSP:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
artifact.downloadProgress.eta = 0 artifact.downloadProgress.eta = 0
self._sync_artifact_progress(artifact) self._sync_artifact_progress(artifact)
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed if self.selected_bundle:
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
self._report_status() self._report_status()
self._download_start_times.pop(artifact.fileName, None) self._download_start_times.pop(artifact.fileName, None)
raise raise
async def _process_model(self, model, destination_path: str) -> None: async def _process_model(self, model, destination_path: str) -> None:
"""Processes a single model download including verification""" """Processes a single model download including verification"""
model_artifact = model.artifact await self._process_artifact(model.artifact, destination_path)
metadata_artifact = model.metadata
await self._process_artifact(metadata_artifact, destination_path)
await self._process_artifact(model_artifact, destination_path)
def _report_status(self) -> None: def _report_status(self) -> None:
"""Reports current status through messaging system""" """Reports current status through messaging system"""
@@ -205,16 +217,16 @@ class ModelManagerSP:
try: try:
seen_artifacts: set[str] = set() seen_artifacts: set[str] = set()
for model in self.selected_bundle.models: for model in self.selected_bundle.models:
for artifact in (model.metadata, model.artifact): artifact = model.artifact
if not artifact.fileName: if not artifact.fileName:
continue continue
if artifact.fileName in seen_artifacts: if artifact.fileName in seen_artifacts:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100 artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0 artifact.downloadProgress.eta = 0
else: else:
seen_artifacts.add(artifact.fileName) seen_artifacts.add(artifact.fileName)
await self._process_artifact(artifact, destination_path) await self._process_artifact(artifact, destination_path)
self.active_bundle = self.selected_bundle self.active_bundle = self.selected_bundle
self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded
@@ -275,8 +287,6 @@ class ModelManagerSP:
for model in self.active_bundle.models: for model in self.active_bundle.models:
if hasattr(model, 'artifact') and model.artifact.fileName: if hasattr(model, 'artifact') and model.artifact.fileName:
active_files.append(model.artifact.fileName) active_files.append(model.artifact.fileName)
if hasattr(model, 'metadata') and model.metadata.fileName:
active_files.append(model.metadata.fileName)
# Remove all files except active ones (including their chunk files) # Remove all files except active ones (including their chunk files)
model_dir = Paths.model_root() model_dir = Paths.model_root()
@@ -1,28 +0,0 @@
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
from openpilot.sunnypilot.models.runners.tinygrad.tinygrad_runner import TinygradRunner, TinygradSplitRunner
from openpilot.sunnypilot.models.runners.constants import ModelType
def get_model_runner() -> ModelRunner:
"""
Factory function to create and return the appropriate ModelRunner instance.
Selects TinygradRunner, choosing TinygradSplitRunner if separate vision/policy
models are detected in the active bundle.
:return: An instance of a ModelRunner subclass (ONNXRunner, TinygradRunner, or TinygradSplitRunner).
"""
bundle = get_active_bundle()
if bundle and bundle.models:
model_types = {m.type.raw for m in bundle.models}
# Check if the bundle uses separate vision and policy models (legacy or new split format)
split_types = {ModelType.vision, ModelType.policy, ModelType.offPolicy, ModelType.onPolicy}
if model_types & split_types:
return TinygradSplitRunner()
# Otherwise, assume a single model (likely supercombo)
if bundle.models:
return TinygradRunner(bundle.models[0].type.raw)
# Default fallback to TinygradRunner with the supercombo type if bundle info is missing/incomplete
return TinygradRunner(ModelType.supercombo)
@@ -1,174 +0,0 @@
from abc import abstractmethod, ABC
import numpy as np
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, Model, SliceDict, SEND_RAW_PRED
from openpilot.common.hardware.hw import Paths
import pickle
CUSTOM_MODEL_PATH = Paths.model_root()
class ModelData:
"""
Stores metadata and configuration for a specific machine learning model.
This class loads model metadata (like input shapes and output slices)
from a pickle file associated with a model instance.
:param model: The machine learning model object containing metadata.
"""
def __init__(self, model: Model):
self.model = model
self.metadata = model.metadata
self.input_shapes: ShapeDict = {}
self.output_slices: SliceDict = {}
if self.metadata:
self._load_metadata()
def _load_metadata(self) -> None:
"""Loads input shapes and output slices from the model's metadata pickle file."""
metadata_path = f"{CUSTOM_MODEL_PATH}/{self.metadata.fileName}"
with open(metadata_path, 'rb') as f:
model_metadata = pickle.load(f)
self.input_shapes = model_metadata.get('input_shapes', {})
self.output_slices = model_metadata.get('output_slices', {})
class ModularRunner(ABC):
"""
Represents a modular runner for handling and slicing model outputs.
This abstract base class is designed to provide an interface for modular
parsing and processing of model outputs. Classes inheriting from it must
implement the specified abstract methods, defining how model outputs
should be handled and stored. The primary goal is to enable structured
parsing of outputs through a dictionary-based method mapping.
:ivar parser_method_dict: Mapping dictionary containing parser methods
for handling specific types of outputs.
:type parser_method_dict: dict
"""
@property
@abstractmethod
def parser_method_dict(self) -> dict:
pass
@parser_method_dict.setter
@abstractmethod
def parser_method_dict(self, value: dict) -> None:
pass
@abstractmethod
def _slice_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
pass
class ModelRunner(ModularRunner):
"""
Abstract base class for managing and executing machine learning models.
Provides a common interface for loading models, preparing inputs, running
inference, and slicing/parsing outputs based on model metadata. Derived
classes implement the specifics of input preparation and model execution
for different frameworks (e.g., Tinygrad, ONNX).
"""
def __init__(self):
"""Initializes the model runner, loading the active model bundle."""
self.is_20hz: bool | None = None
self.is_20hz_3d: bool | None = None
self.models: dict[int, ModelData] = {}
self._model_data: ModelData | None = None # Active model data for current operation
self._parser_method_dict: dict = {}
self.inputs: dict = {}
self._parser = None
self._load_models()
self._constants = None
@property
def constants(self):
return self._constants
@property
def parser_method_dict(self) -> dict:
"""Returns the dictionary mapping model types to their respective parsing methods."""
return self._parser_method_dict
@parser_method_dict.setter
def parser_method_dict(self, value: dict) -> None:
"""Sets the dictionary mapping model types to their respective parsing methods."""
self._parser_method_dict = value
def _load_models(self) -> None:
"""Loads the active model bundle configuration and sets up ModelData."""
bundle = get_active_bundle()
if not bundle:
raise ValueError("No active model bundle found, why are we being executed?")
self.models = {model.type.raw: ModelData(model) for model in bundle.models}
self.is_20hz = bundle.is20hz
self.is_20hz_3d = False
@property
def input_shapes(self) -> ShapeDict:
"""Returns the input shapes for the currently active model."""
if self._model_data:
return self._model_data.input_shapes
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
@property
def output_slices(self) -> SliceDict:
"""Returns the output slices for the currently active model."""
if self._model_data:
return self._model_data.output_slices
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
@property
def vision_input_names(self) -> list[str]:
"""Returns the list of vision input names from the input shapes."""
if self._model_data:
return list(self._model_data.input_shapes.keys())
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
@abstractmethod
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
"""
Abstract method to prepare inputs for model inference.
:param numpy_inputs: Dictionary of numpy arrays for non-image inputs.
:return: Dictionary of prepared inputs ready for the model.
"""
raise NotImplementedError
@abstractmethod
def _run_model(self) -> NumpyDict:
"""
Abstract method to execute model inference with prepared inputs.
:return: Dictionary containing the model's raw output arrays.
"""
raise NotImplementedError
def _slice_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""
Slices the raw model output array based on the output_slices metadata.
:param model_outputs: The raw numpy array output from the model.
:return: A dictionary where keys are output names and values are sliced numpy arrays.
"""
if not self._model_data:
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
sliced_outputs = {k: model_outputs[np.newaxis, v] for k, v in self._model_data.output_slices.items()}
if SEND_RAW_PRED:
sliced_outputs['raw_pred'] = model_outputs.copy() # Optionally include the full raw output
return sliced_outputs
def run_model(self) -> NumpyDict:
"""
Executes the model inference pipeline: runs the model and parses outputs.
:return: Dictionary containing the final parsed model outputs.
"""
return self._run_model() # Parsing is handled within specific runner implementations
@@ -1,91 +0,0 @@
import os
from abc import ABC
import numpy as np
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
from openpilot.sunnypilot.models.runners.constants import ModelType, NumpyDict
from openpilot.sunnypilot.models.runners.model_runner import ModularRunner
from openpilot.common.hardware.hw import Paths
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
CUSTOM_MODEL_PATH = Paths.model_root()
class OffPolicyTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for off-policy models.
Uses a SplitParser to handle outputs specific to the off-policy part of a split model setup.
"""
def __init__(self):
self._off_policy_parser = SplitParser()
self.parser_method_dict[ModelType.offPolicy] = self._parse_off_policy_outputs
def _parse_off_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses off-policy model outputs using SplitParser."""
result: NumpyDict = self._off_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
return result
class OnPolicyTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for on-policy models.
Uses a SplitParser to handle outputs specific to the on-policy part of a split model setup.
"""
def __init__(self):
self._on_policy_parser = SplitParser()
self.parser_method_dict[ModelType.onPolicy] = self._parse_on_policy_outputs
def _parse_on_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses on-policy model outputs using SplitParser."""
result: NumpyDict = self._on_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
return result
class PolicyTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for policy-only models.
Uses a SplitParser to handle outputs specific to the policy part of a split model setup.
"""
def __init__(self):
self._policy_parser = SplitParser()
self.parser_method_dict[ModelType.policy] = self._parse_policy_outputs
def _parse_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses policy model outputs using SplitParser."""
result: NumpyDict = self._policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
return result
class VisionTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for vision-only models.
Uses a SplitParser to handle outputs specific to the vision part of a split model setup.
"""
def __init__(self):
self._vision_parser = SplitParser()
self.parser_method_dict[ModelType.vision] = self._parse_vision_outputs
def _parse_vision_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses vision model outputs using SplitParser."""
result: NumpyDict = self._vision_parser.parse_vision_outputs(self._slice_outputs(model_outputs))
return result
class SupercomboTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for vision-only models.
Uses a SplitParser to handle outputs specific to the vision part of a split model setup.
"""
def __init__(self):
self._supercombo_parser = CombinedParser()
self.parser_method_dict[ModelType.supercombo] = self._parse_supercombo_outputs
def _parse_supercombo_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses vision model outputs using SplitParser."""
result: NumpyDict = self._supercombo_parser.parse_outputs(self._slice_outputs(model_outputs))
return result
@@ -1,179 +0,0 @@
import pickle
import numpy as np
from openpilot.sunnypilot.models.runners.constants import NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad, OffPolicyTinygrad, OnPolicyTinygrad
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
from tinygrad.tensor import Tensor
class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad, OnPolicyTinygrad):
"""
A ModelRunner implementation for executing Tinygrad models.
Handles loading Tinygrad model artifacts (.pkl), preparing inputs as Tinygrad
Tensors (potentially using QCOM extensions on TICI), running inference,
and parsing the outputs.
:param model_type: The type of model (e.g., supercombo) to load and run.
"""
def __init__(self, model_type: int = ModelType.supercombo):
ModelRunner.__init__(self)
SupercomboTinygrad.__init__(self)
PolicyTinygrad.__init__(self)
VisionTinygrad.__init__(self)
OffPolicyTinygrad.__init__(self)
OnPolicyTinygrad.__init__(self)
self._constants = ModelConstants
self._model_data = self.models.get(model_type)
if not self._model_data or not self._model_data.model:
raise ValueError(f"Model data for type {model_type} not available.")
artifact_filename = self._model_data.model.artifact.fileName
assert artifact_filename.endswith('_tinygrad.pkl'), \
f"Invalid model file {artifact_filename} for TinygradRunner"
model_pkl_path = f"{CUSTOM_MODEL_PATH}/{artifact_filename}"
with open(model_pkl_path, "rb") as f:
try:
# Load the compiled Tinygrad model runner function
self.model_run = pickle.load(f)
except FileNotFoundError as e:
# Provide a helpful error message if the model was built for a different platform
assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC"
raise
# Map input names to their required dtype and device from the loaded model
self.input_to_dtype = {}
self.input_to_device = {}
for idx, name in enumerate(self.model_run.captured.expected_names):
info = self.model_run.captured.expected_input_info[idx]
self.input_to_dtype[name] = info[2] # dtype
self.input_to_device[name] = info[3] # device
self._policy_cached = False
@property
def vision_input_names(self) -> list[str]:
"""Returns the list of vision input names from the input shapes."""
return [name for name in self.input_shapes.keys() if 'img' in name]
def prepare_policy_inputs(self, numpy_inputs: NumpyDict):
if not self._policy_cached:
for key, value in numpy_inputs.items():
self.inputs[key] = Tensor(value, device='NPY').realize()
self._policy_cached = True
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
"""Prepares all vision and policy inputs for the model."""
self.prepare_policy_inputs(numpy_inputs)
for key in self.vision_input_names:
if key in self.inputs:
self.inputs[key] = self.inputs[key].cast(self.input_to_dtype[key])
return self.inputs
def _run_model(self) -> NumpyDict:
"""Runs the Tinygrad model inference and parses the outputs."""
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
return self._parse_outputs(outputs)
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses the raw model outputs using the standard Parser."""
if self._model_data is None:
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
result: NumpyDict = self.parser_method_dict[self._model_data.model.type.raw](model_outputs)
return result
class TinygradSplitRunner(ModelRunner):
"""
A ModelRunner that coordinates separate TinygradVisionRunner and TinygradPolicyRunner instances.
Manages the execution of split vision and policy models, combining their inputs and outputs.
"""
def __init__(self):
super().__init__()
self.is_20hz_3d = True
self.vision_runner = TinygradRunner(ModelType.vision)
self.policy_runner = TinygradRunner(ModelType.policy) if self.models.get(ModelType.policy) else None
self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None
self.on_policy_runner = TinygradRunner(ModelType.onPolicy) if self.models.get(ModelType.onPolicy) else None
self._constants = SplitModelConstants
def _run_model(self) -> NumpyDict:
"""Runs both vision and policy models and merges their parsed outputs."""
vision_output = self.vision_runner.run_model()
outputs = {**vision_output}
if self.policy_runner:
policy_output = self.policy_runner.run_model()
outputs.update(policy_output)
if self.off_policy_runner:
off_policy_output = self.off_policy_runner.run_model()
if self.on_policy_runner:
off_policy_output.pop('plan', None)
outputs.update(off_policy_output)
if self.on_policy_runner:
on_policy_output = self.on_policy_runner.run_model()
outputs.update(on_policy_output)
if 'planplus' in outputs and 'plan' in outputs:
outputs['plan'] = outputs['plan'] + outputs['planplus']
return outputs
@property
def vision_input_names(self) -> list[str]:
"""Returns the list of vision input names from the vision runner."""
return list(self.vision_runner.vision_input_names)
@property
def input_shapes(self) -> ShapeDict:
"""Returns the combined input shapes from both vision and policy models."""
shapes = {**self.vision_runner.input_shapes}
if self.policy_runner:
shapes.update(self.policy_runner.input_shapes)
if self.off_policy_runner:
shapes.update(self.off_policy_runner.input_shapes)
if self.on_policy_runner:
shapes.update(self.on_policy_runner.input_shapes)
return shapes
@property
def output_slices(self) -> SliceDict:
"""Returns the combined output slices from both vision and policy models."""
slices = {**self.vision_runner.output_slices}
if self.policy_runner:
slices.update(self.policy_runner.output_slices)
if self.off_policy_runner:
slices.update(self.off_policy_runner.output_slices)
if self.on_policy_runner:
slices.update(self.on_policy_runner.output_slices)
return slices
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
"""Prepares inputs for both vision and policy models."""
if self.policy_runner:
self.policy_runner.prepare_policy_inputs(numpy_inputs)
for key in self.vision_input_names:
if key in self.inputs:
self.vision_runner.inputs[key] = self.inputs[key].cast(self.vision_runner.input_to_dtype[key])
inputs = {**self.vision_runner.inputs}
if self.policy_runner:
inputs.update(self.policy_runner.inputs)
if self.off_policy_runner:
self.off_policy_runner.prepare_policy_inputs(numpy_inputs)
inputs.update(self.off_policy_runner.inputs)
if self.on_policy_runner:
self.on_policy_runner.prepare_policy_inputs(numpy_inputs)
inputs.update(self.on_policy_runner.inputs)
return inputs
@@ -43,6 +43,7 @@ class SplitModelConstants:
LANE_LINES_WIDTH = 2 LANE_LINES_WIDTH = 2
ROAD_EDGES_WIDTH = 2 ROAD_EDGES_WIDTH = 2
PLAN_WIDTH = 15 PLAN_WIDTH = 15
ACTION_WIDTH = 2
DESIRE_PRED_WIDTH = 8 DESIRE_PRED_WIDTH = 8
LAT_PLANNER_SOLUTION_WIDTH = 4 LAT_PLANNER_SOLUTION_WIDTH = 4
DESIRED_CURV_WIDTH = 1 DESIRED_CURV_WIDTH = 1
@@ -73,10 +73,16 @@ def _cleanup_unsupported_params(CP: structs.CarParams, CP_SP: structs.CarParamsS
if params is None: if params is None:
params = Params() params = Params()
if params.get_bool("LateralJerkTorqueController") and params.get_bool("NeuralNetworkLateralControl"):
cloudlog.warning("LateralJerkTorqueController and NeuralNetworkLateralControl both enabled, disabling both")
params.put_bool("LateralJerkTorqueController", False, block=True)
params.put_bool("NeuralNetworkLateralControl", False, block=True)
if CP.steerControlType == structs.CarParams.SteerControlType.angle: if CP.steerControlType == structs.CarParams.SteerControlType.angle:
cloudlog.warning("SteerControlType is angle, cleaning up params") cloudlog.warning("SteerControlType is angle, cleaning up params")
params.remove("NeuralNetworkLateralControl") params.remove("NeuralNetworkLateralControl")
params.remove("EnforceTorqueControl") params.remove("EnforceTorqueControl")
params.remove("LateralJerkTorqueController")
if not CP_SP.intelligentCruiseButtonManagementAvailable or CP.openpilotLongitudinalControl: if not CP_SP.intelligentCruiseButtonManagementAvailable or CP.openpilotLongitudinalControl:
cloudlog.warning("ICBM not available or openpilot Longitudinal Control enabled, cleaning up params") cloudlog.warning("ICBM not available or openpilot Longitudinal Control enabled, cleaning up params")
@@ -123,6 +129,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
# tesla # tesla
keys.extend([ keys.extend([
"TeslaCoopSteering", "TeslaCoopSteering",
"TeslaMadsScreenButton",
]) ])
# toyota # toyota
@@ -1,5 +1,5 @@
""" """
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors. Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
@@ -33,6 +33,7 @@ class LatControlTorqueExt(NeuralNetworkLateralControl, LatControlTorqueExtOverri
self._output_torque = output_torque self._output_torque = output_torque
self.update_calculations(CS, VM, desired_lateral_accel) self.update_calculations(CS, VM, desired_lateral_accel)
self.update_jerk_aware_torque_control(CS, roll_compensation, gravity_adjusted_lateral_accel)
self.update_neural_network_feedforward(CS, params, calibrated_pose) self.update_neural_network_feedforward(CS, params, calibrated_pose)
return self._pid_log, self._output_torque return self._pid_log, self._output_torque
@@ -132,3 +132,10 @@ class LatControlTorqueExtBase:
self.lat_accel_friction_factor = 1.0 self.lat_accel_friction_factor = 1.0
self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk
self.lateral_jerk_measurement = self.lat_jerk_friction_factor * self.actual_lateral_jerk self.lateral_jerk_measurement = self.lat_jerk_friction_factor * self.actual_lateral_jerk
def update_output_torque(self, CS):
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
self._output_torque = self._pid.update(self._pid_log.error,
feedforward=self._ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
@@ -0,0 +1,45 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.car.lateral import FRICTION_THRESHOLD
from opendbc.sunnypilot.car.interfaces import LatControlInputs
from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space
from openpilot.common.params import Params
from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase
class LatControlTorqueJerkAware(LatControlTorqueExtBase):
def __init__(self, lac_torque, CP, CP_SP, CI):
super().__init__(lac_torque, CP, CP_SP, CI)
self.params = Params()
self._jerk_aware_enabled = self.params.get_bool("LateralJerkTorqueController")
def update_limits(self):
if not self._jerk_aware_enabled:
return
self._pid.set_limits(self.lac_torque.steer_max, -self.lac_torque.steer_max)
def update_jerk_aware_torque_control(self, CS, roll_compensation, gravity_adjusted_lateral_accel):
if not self._jerk_aware_enabled:
return
torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(
LatControlInputs(self._setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False
)
torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(
LatControlInputs(self._measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False
)
self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) # ty: ignore[invalid-assignment]
self._ff = self.torque_from_lateral_accel_in_torque_space(
LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True
)
friction_input = self.update_friction_input(self._desired_lateral_accel, self._actual_lateral_accel)
self._ff += get_friction_in_torque_space(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
self.update_output_torque(CS)
@@ -82,7 +82,7 @@ class LatControlTorque(LatControl):
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
self.lat_accel_request_buffer.append(future_desired_lateral_accel) self.lat_accel_request_buffer.append(future_desired_lateral_accel)
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / max(lat_delay, self.dt)
measurement = measured_curvature * CS.vEgo ** 2 measurement = measured_curvature * CS.vEgo ** 2
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
@@ -14,7 +14,8 @@ from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_t
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase, sign from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import sign
from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_jerk_aware import LatControlTorqueJerkAware
from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH
from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel
@@ -31,17 +32,18 @@ def roll_pitch_adjust(roll, pitch):
return roll * math.cos(pitch) return roll * math.cos(pitch)
class NeuralNetworkLateralControl(LatControlTorqueExtBase): class NeuralNetworkLateralControl(LatControlTorqueJerkAware):
def __init__(self, lac_torque, CP, CP_SP, CI): def __init__(self, lac_torque, CP, CP_SP, CI):
super().__init__(lac_torque, CP, CP_SP, CI) super().__init__(lac_torque, CP, CP_SP, CI)
self.params = Params() self.params = Params()
self.enabled = self.params.get_bool("NeuralNetworkLateralControl") self.enabled = self.params.get_bool("NeuralNetworkLateralControl")
self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH model_path = CP_SP.neuralNetworkLateralControl.model.path
self.has_nn_model = model_path not in (MOCK_MODEL_PATH, '')
# NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data # NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data
# of lat accel and roll # of lat accel and roll
# Past value is computed using previous desired lat accel and observed roll # Past value is computed using previous desired lat accel and observed roll
self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path) self.model = NNTorqueModel(model_path) if self.has_nn_model else None
self.pitch = FirstOrderFilter(0.0, 0.5, 0.01) self.pitch = FirstOrderFilter(0.0, 0.5, 0.01)
self.pitch_last = 0.0 self.pitch_last = 0.0
@@ -64,6 +66,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
return self.enabled and self.model_valid and self.has_nn_model return self.enabled and self.model_valid and self.has_nn_model
def update_limits(self): def update_limits(self):
super().update_limits()
if not self._nnlc_enabled: if not self._nnlc_enabled:
return return
@@ -84,13 +87,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone, self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
FRICTION_THRESHOLD, self.torque_params) FRICTION_THRESHOLD, self.torque_params)
def update_output_torque(self, CS):
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
self._output_torque = self._pid.update(self._pid_log.error,
feedforward=self._ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None: def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None:
if not self._nnlc_enabled: if not self._nnlc_enabled:
return return
@@ -0,0 +1,98 @@
"""
Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
NEARSIDE_PROB = 0.2
EDGE_PROB = 0.35
EDGE_REACTION_TIME = 1.0
EDGE_CLEAR_TIME = 0.3
MIN_SPEED = 20 * CV.MPH_TO_MS
VEHICLE_EDGE_MARGIN = 1.08
EDGE_CLEARANCE = 3.7
class RoadEdgeLaneChangeController:
def __init__(self):
self.params = Params()
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
self.param_read_counter = 0
self.left_edge_detected = False
self.right_edge_detected = False
self.left_edge_timer = 0.0
self.right_edge_timer = 0.0
self.left_clear_timer = 0.0
self.right_clear_timer = 0.0
def read_params(self) -> None:
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
def update_params(self) -> None:
if self.param_read_counter % 50 == 0:
self.read_params()
self.param_read_counter += 1
def reset(self) -> None:
self.left_edge_detected = False
self.right_edge_detected = False
self.left_edge_timer = 0.0
self.right_edge_timer = 0.0
self.left_clear_timer = 0.0
self.right_clear_timer = 0.0
def update(self, road_edge_stds, lane_line_probs, v_ego: float, road_edges=None) -> None:
self.update_params()
if not self.enabled or v_ego < MIN_SPEED:
self.reset()
return
left_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
right_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
left_lane_prob = lane_line_probs[0]
right_lane_prob = lane_line_probs[3]
if road_edges is not None and len(road_edges) == 2 and len(road_edges[0].y) > 0 and len(road_edges[1].y) > 0:
left_clearance = abs(road_edges[0].y[0]) - VEHICLE_EDGE_MARGIN
right_clearance = abs(road_edges[1].y[0]) - VEHICLE_EDGE_MARGIN
else:
left_clearance = 0.0
right_clearance = 0.0
left_cond = left_edge_prob > EDGE_PROB and left_lane_prob < NEARSIDE_PROB and left_clearance < EDGE_CLEARANCE
right_cond = right_edge_prob > EDGE_PROB and right_lane_prob < NEARSIDE_PROB and right_clearance < EDGE_CLEARANCE
if left_cond:
self.left_edge_timer = min(self.left_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
self.left_clear_timer = 0.0
if self.left_edge_timer > EDGE_REACTION_TIME:
self.left_edge_detected = True
else:
self.left_clear_timer += DT_MDL
if self.left_clear_timer > EDGE_CLEAR_TIME:
self.left_edge_timer = 0.0
self.left_edge_detected = False
if right_cond:
self.right_edge_timer = min(self.right_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
self.right_clear_timer = 0.0
if self.right_edge_timer > EDGE_REACTION_TIME:
self.right_edge_detected = True
else:
self.right_clear_timer += DT_MDL
if self.right_clear_timer > EDGE_CLEAR_TIME:
self.right_edge_timer = 0.0
self.right_edge_detected = False
def update_and_fill(self, modelv2, mdv2sp, v_ego):
self.update(modelv2.roadEdgeStds, modelv2.laneLineProbs, v_ego, modelv2.roadEdges)
mdv2sp.leftLaneChangeEdgeBlock = self.left_edge_detected
mdv2sp.rightLaneChangeEdgeBlock = self.right_edge_detected
return self.left_edge_detected, self.right_edge_detected
@@ -151,8 +151,8 @@ class SmartCruiseControlMap:
a = 0.5 * TARGET_JERK a = 0.5 * TARGET_JERK
b = self.a_ego b = self.a_ego
c = self.v_ego - tv c = self.v_ego - tv
t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / 2 * a t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / (2 * a)
t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / 2 * a t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / (2 * a)
if not isinstance(t_a, complex) and t_a > 0: if not isinstance(t_a, complex) and t_a > 0:
t = t_a t = t_a
else: else:
@@ -4,13 +4,17 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
import json
import math
import platform import platform
import pytest
from openpilot.cereal import custom from openpilot.cereal import custom
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import SmartCruiseControlMap from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import R, SmartCruiseControlMap
MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState
@@ -55,4 +59,17 @@ class TestSmartCruiseControlMap:
self.scc_m.update(True, False, 0., 0., 0.) self.scc_m.update(True, False, 0., 0., 0.)
assert self.scc_m.state == VisionState.enabled assert self.scc_m.state == VisionState.enabled
def test_moderate_curve(self):
# Regression: `... / 2 * a` parsed as `(.../2)*a` instead of `.../(2*a)`,
# making max_d ~11x too small so the moderate-curve branch never tripped.
# v_ego=25, a_ego=0, tv=24: fixed max_d≈45m vs buggy ≈4m at a 40m waypoint.
waypoint_lon_deg = (40.0 / R) * (180.0 / math.pi)
self.mem_params.put("LastGPSPosition", json.dumps({"latitude": 0.0, "longitude": 0.0}), block=True)
self.mem_params.put("MapTargetVelocities",
json.dumps([{"latitude": 0.0, "longitude": waypoint_lon_deg, "velocity": 24.0}]), block=True)
self.scc_m.update(True, False, 25.0, 0.0, 30.0)
assert self.scc_m.v_target == pytest.approx(24.0)
# TODO-SP: mock data from modelV2 to test other states # TODO-SP: mock data from modelV2 to test other states
@@ -91,7 +91,7 @@ class SpeedLimitAssist:
self._plus_hold = 0. self._plus_hold = 0.
self._minus_hold = 0. self._minus_hold = 0.
self._last_carstate_ts = 0. self._release_toggle_prev = 0
# TODO-SP: SLA's own output_a_target for planner # TODO-SP: SLA's own output_a_target for planner
# Solution functions mapped to respective states # Solution functions mapped to respective states
@@ -146,16 +146,16 @@ class SpeedLimitAssist:
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params) set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
def update_car_state(self, CS: car.CarState) -> None: def update_buttons(self, release_toggle: int) -> None:
released = self._release_toggle_prev ^ release_toggle
self._release_toggle_prev = release_toggle
if not released:
return
now = time.monotonic() now = time.monotonic()
self._last_carstate_ts = now if any((released >> b) & 1 for b in CRUISE_BUTTONS_PLUS):
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
for b in CS.buttonEvents: if any((released >> b) & 1 for b in CRUISE_BUTTONS_MINUS):
if not b.pressed: self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
if b.type in CRUISE_BUTTONS_PLUS:
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
elif b.type in CRUISE_BUTTONS_MINUS:
self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool: def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool:
now = time.monotonic() now = time.monotonic()
@@ -5,11 +5,14 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
import time
import pytest import pytest
from openpilot.cereal import custom from openpilot.cereal import custom
from opendbc.car.car_helpers import interfaces from opendbc.car.car_helpers import interfaces
from opendbc.car.rivian.values import CAR as RIVIAN from opendbc.car.rivian.values import CAR as RIVIAN
from opendbc.car.structs import car
from opendbc.car.tesla.values import CAR as TESLA from opendbc.car.tesla.values import CAR as TESLA
from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.common.constants import CV from openpilot.common.constants import CV
@@ -21,9 +24,13 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, CRUISE_BUTTON_CONFIRM_HOLD
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values()) ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
@@ -276,3 +283,86 @@ class TestSpeedLimitAssist:
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active] assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
elif initial_state in ACTIVE_STATES: elif initial_state in ACTIVE_STATES:
assert self.sla.state in ACTIVE_STATES assert self.sla.state in ACTIVE_STATES
class TestButtonStateTrackerSLAIntegration:
def setup_method(self, method):
self.tracker = ButtonStateTracker()
self.params = Params()
self.params.put("IsReleaseSpBranch", True, block=True)
self.params.put("SpeedLimitMode", int(Mode.assist), block=True)
self.params.put_bool("IsMetric", False, block=True)
self.params.put("SpeedLimitOffsetType", 0, block=True)
self.params.put("SpeedLimitValueOffset", 0, block=True)
CarInterface = interfaces[DEFAULT_CAR]
CP = CarInterface.get_non_essential_params(DEFAULT_CAR)
CP.openpilotLongitudinalControl = True
CP_SP = CarInterface.get_non_essential_params_sp(CP, DEFAULT_CAR)
self.sla = SpeedLimitAssist(CP, CP_SP)
def _make_cs(self, events=None) -> car.CarState:
CS = car.CarState()
CS.buttonEvents = events or []
return CS
def _run_ctrl_frames(self, frames: list[car.CarState]) -> None:
for cs in frames:
self.tracker.update(cs)
def test_button_confirm_via_tracker(self) -> None:
self._run_ctrl_frames([
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]),
self._make_cs(),
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]),
self._make_cs(),
self._make_cs(),
])
self.sla.update_buttons(self.tracker.release_toggle)
assert self.sla._get_button_release(req_plus=True, req_minus=False)
def test_rapid_press_release_between_polls(self) -> None:
self.sla.update_buttons(self.tracker.release_toggle)
self._run_ctrl_frames([
self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]),
self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=False)]),
self._make_cs(),
self._make_cs(),
self._make_cs(),
])
self.sla.update_buttons(self.tracker.release_toggle)
assert self.sla._get_button_release(req_plus=False, req_minus=True)
def test_multiple_releases_between_polls(self) -> None:
self.sla.update_buttons(self.tracker.release_toggle)
self._run_ctrl_frames([
self._make_cs([
ButtonEvent(type=ButtonType.accelCruise, pressed=True),
ButtonEvent(type=ButtonType.decelCruise, pressed=True),
]),
self._make_cs([
ButtonEvent(type=ButtonType.accelCruise, pressed=False),
ButtonEvent(type=ButtonType.decelCruise, pressed=False),
]),
])
self.sla.update_buttons(self.tracker.release_toggle)
assert self.sla._get_button_release(req_plus=True, req_minus=False)
assert self.sla._get_button_release(req_plus=False, req_minus=True)
def test_no_false_positive_same_toggle(self) -> None:
self.sla.update_buttons(self.tracker.release_toggle)
self.sla.update_buttons(self.tracker.release_toggle)
assert not self.sla._get_button_release(req_plus=True, req_minus=False)
assert not self.sla._get_button_release(req_plus=False, req_minus=True)
def test_button_confirm_expires(self) -> None:
self._run_ctrl_frames([
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]),
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]),
])
self.sla.update_buttons(self.tracker.release_toggle)
time.sleep(CRUISE_BUTTON_CONFIRM_HOLD + 0.1)
assert not self.sla._get_button_release(req_plus=True, req_minus=False)
@@ -2,10 +2,11 @@ import pytest
from openpilot.cereal import log, custom from openpilot.cereal import log, custom
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
TurnDirection = custom.ModelDataV2SP.TurnDirection TurnDirection = custom.ModelDataV2SP.TurnDirection
@@ -109,5 +110,17 @@ def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, e
dh = DesireHelper() dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10): for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob) dh.update(carstate, lateral_active, lane_change_prob,
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers left_edge_detected=False, right_edge_detected=False)
assert dh.desire == expected_desire
def test_edge_blocks_lane_change(set_lane_turn_params):
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.lane_change_direction == LaneChangeDirection.left
assert dh.desire == log.Desire.none
@@ -0,0 +1,108 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from openpilot.cereal import log, messaging
from opendbc.car.structs import car
from opendbc.car.car_helpers import interfaces
from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.vehicle_model import VehicleModel
from openpilot.common.params import Params
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car.helpers import convert_to_capnp
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.locationd.helpers import Pose
from openpilot.common.mock.generators import generate_livePose
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
from openpilot.selfdrive.modeld.constants import ModelConstants
def _make_controller(enhanced=False, nnlc=False):
params = Params()
params.put_bool("EnforceTorqueControl", True, block=True)
params.put_bool("LateralJerkTorqueController", enhanced, block=True)
params.put_bool("NeuralNetworkLateralControl", nnlc, block=True)
car_name = HONDA.HONDA_CIVIC
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
CI = CarInterface(CP, CP_SP)
sunnypilot_interfaces.setup_interfaces(CI, params)
CP_SP = convert_to_capnp(CP_SP)
VM = VehicleModel(CP)
controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
return controller, VM, CP
def _make_model_v2():
model = messaging.new_message('modelV2')
position = log.XYZTData.new_message()
position.x = [float(x) for x in 30.0 * np.array(ModelConstants.T_IDXS)]
model.modelV2.position = position
orientation = log.XYZTData.new_message()
orientation.x = [0.0 for _ in ModelConstants.T_IDXS]
orientation.y = [0.0 for _ in ModelConstants.T_IDXS]
model.modelV2.orientation = orientation
velocity = log.XYZTData.new_message()
velocity.x = [30.0 for _ in ModelConstants.T_IDXS]
model.modelV2.velocity = velocity
acceleration = log.XYZTData.new_message()
acceleration.x = [0.0 for _ in ModelConstants.T_IDXS]
acceleration.y = [0.0 for _ in ModelConstants.T_IDXS]
model.modelV2.acceleration = acceleration
return model
def _run_update(controller, VM):
CS = car.CarState.new_message()
CS.vEgo = 30
CS.steeringPressed = False
lp = generate_livePose()
pose = Pose.from_live_pose(lp.livePose)
params = log.LiveParametersData.new_message()
model_v2 = _make_model_v2().modelV2
controller.extension.update_model_v2(model_v2)
controller.extension.update_lateral_lag(0.2)
return controller.update(True, CS, VM, params, False, 0.5, pose, False, 0.2)
class TestLatControlTorqueExt:
def test_init_enhanced_only(self):
controller, VM, _ = _make_controller(enhanced=True, nnlc=False)
assert controller.extension._jerk_aware_enabled
assert not controller.extension.enabled # NNLC disabled
def test_init_nnlc_only(self):
controller, VM, _ = _make_controller(enhanced=False, nnlc=True)
assert not controller.extension._jerk_aware_enabled
assert controller.extension.enabled
def test_init_neither(self):
controller, VM, _ = _make_controller(enhanced=False, nnlc=False)
assert not controller.extension._jerk_aware_enabled
assert not controller.extension.enabled
def test_init_both_no_crash(self):
controller, VM, _ = _make_controller(enhanced=True, nnlc=True)
assert not controller.extension._jerk_aware_enabled
assert not controller.extension.enabled
def test_update_enhanced_only(self):
controller, VM, _ = _make_controller(enhanced=True, nnlc=False)
output_torque, _, pid_log = _run_update(controller, VM)
assert pid_log.active
def test_update_neither(self):
controller, VM, _ = _make_controller(enhanced=False, nnlc=False)
output_torque, _, pid_log = _run_update(controller, VM)
assert pid_log.active
def test_update_both_no_crash(self):
controller, VM, _ = _make_controller(enhanced=True, nnlc=True)
output_torque, _, pid_log = _run_update(controller, VM)
assert pid_log.active
@@ -0,0 +1,169 @@
"""
Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pytest
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.relc import (
RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED,
VEHICLE_EDGE_MARGIN, EDGE_CLEARANCE,
)
V_HIGH = MIN_SPEED + 2.0
V_LOW = MIN_SPEED - 1.0
class MockEdge:
def __init__(self, y_val):
self.y = [y_val] * 33
def edges(left_y, right_y):
return [MockEdge(left_y), MockEdge(right_y)]
CLOSE_EDGES = edges(-2.0, 1.5)
FAR_EDGES = edges(-10.0, 10.0)
@pytest.fixture
def relc(mocker):
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
controller = RoadEdgeLaneChangeController()
controller.enabled = True
return controller
def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, road_edges=CLOSE_EDGES):
for _ in range(int(seconds / DT_MDL) + 1):
controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges)
@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [
([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], "left_edge_detected"),
([0.9, 0.0], [0.8, 0.8, 0.8, 0.0], "right_edge_detected"),
])
def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr):
drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1)
assert getattr(relc, attr)
def test_edge_detection_requires_time(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME - 0.05)
assert not relc.left_edge_detected
def test_both_edges_detected(relc):
drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
assert relc.right_edge_detected
def test_noise_doesnt_clear(relc):
edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8])
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
relc.update(*clear, V_HIGH, CLOSE_EDGES)
relc.update(*edge, V_HIGH, CLOSE_EDGES)
assert relc.left_edge_detected
def test_clears_after_window(relc):
edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8])
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
drive(relc, *clear, EDGE_CLEAR_TIME + 0.05)
assert not relc.left_edge_detected
assert relc.left_edge_timer == 0.0
def test_low_speed_skips(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW)
assert not relc.left_edge_detected
assert relc.left_edge_timer == 0.0
def test_speed_drop_resets(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_LOW, CLOSE_EDGES)
assert not relc.left_edge_detected
def test_param_off_resets(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
relc.params.get_bool.return_value = False
relc.read_params()
relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_HIGH, CLOSE_EDGES)
assert not relc.left_edge_detected
assert not relc.right_edge_detected
def test_lane_line_prevents_detection(relc):
drive(relc, [0.0, 0.9], [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
assert not relc.left_edge_detected
def test_one_side_blocks_other_allows(relc):
drive(relc, [0.9, 0.0], [0.8, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
assert relc.right_edge_detected
assert not relc.left_edge_detected
def test_disabled_no_detection(relc):
relc.enabled = False
relc.params.get_bool.return_value = False
drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
assert not relc.left_edge_detected
assert not relc.right_edge_detected
def test_far_edge_no_block(relc):
drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, road_edges=FAR_EDGES)
assert not relc.left_edge_detected
def test_close_edge_blocks(relc):
drive(relc, [0.9, 0.0], [0.05, 0.8, 0.8, 0.05], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-8.0, 1.5))
assert relc.right_edge_detected
assert not relc.left_edge_detected
def test_wide_road_no_lines_no_block(relc):
drive(relc, [0.0, 0.0], [0.05, 0.4, 0.4, 0.05], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-8.0, 8.0))
assert not relc.left_edge_detected
assert not relc.right_edge_detected
def test_narrow_road_both_block(relc):
drive(relc, [0.0, 0.0], [0.02, 0.4, 0.4, 0.02], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-2.5, 2.5))
assert relc.left_edge_detected
assert relc.right_edge_detected
def test_clearance_boundary(relc):
boundary = VEHICLE_EDGE_MARGIN + EDGE_CLEARANCE # 4.78m
drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-(boundary - 0.1), 10.0))
assert relc.left_edge_detected
relc.reset()
drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-(boundary + 0.1), 10.0))
assert not relc.left_edge_detected
@@ -0,0 +1,26 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.car import structs
class ButtonStateTracker:
def __init__(self) -> None:
self.pressed: int = 0
self.release_toggle: int = 0
def update(self, CS: structs.CarState) -> None:
for b in CS.buttonEvents:
bit = 1 << b.type.raw
if b.pressed:
self.pressed |= bit
else:
self.pressed &= ~bit
self.release_toggle ^= bit
def publish(self, ss_sp) -> None:
ss_sp.buttonsPressed = self.pressed
ss_sp.buttonsReleaseToggle = self.release_toggle
@@ -244,4 +244,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.normal, AlertSize.none, AlertStatus.normal, AlertSize.none,
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.), Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
}, },
EventNameSP.laneChangeRoadEdge: {
ET.WARNING: Alert(
"Lane Change Unavailable: Road Edge",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
},
} }
@@ -0,0 +1,67 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.car.structs import car
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
class TestButtonStateTracker:
def setup_method(self) -> None:
self.tracker = ButtonStateTracker()
def make_cs(self, events: list) -> car.CarState:
CS = car.CarState()
CS.buttonEvents = events
return CS
def test_initial_state(self) -> None:
assert self.tracker.pressed == 0
assert self.tracker.release_toggle == 0
def test_press_sets_bit(self) -> None:
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
assert self.tracker.pressed == (1 << ButtonType.accelCruise)
assert self.tracker.release_toggle == 0
def test_release_clears_and_toggles(self) -> None:
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
assert self.tracker.pressed == 0
assert self.tracker.release_toggle == (1 << ButtonType.accelCruise)
def test_multiple_buttons(self) -> None:
self.tracker.update(self.make_cs([
ButtonEvent(type=ButtonType.accelCruise, pressed=True),
ButtonEvent(type=ButtonType.decelCruise, pressed=True),
]))
assert self.tracker.pressed == (1 << ButtonType.accelCruise) | (1 << ButtonType.decelCruise)
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
assert self.tracker.pressed == (1 << ButtonType.decelCruise)
assert self.tracker.release_toggle == (1 << ButtonType.accelCruise)
def test_release_toggle_flips(self) -> None:
for _ in range(2):
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=False)]))
assert self.tracker.release_toggle == 0
def test_publish(self) -> None:
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
class MockSP:
buttonsPressed = 0
buttonsReleaseToggle = 0
sp = MockSP()
self.tracker.publish(sp)
assert sp.buttonsPressed == self.tracker.pressed
assert sp.buttonsReleaseToggle == self.tracker.release_toggle
@@ -323,6 +323,32 @@
"equals": true "equals": true
}, },
"items": [ "items": [
{
"key": "LateralJerkTorqueController",
"widget": "toggle",
"title": "Lateral Jerk Torque Controller",
"description": "Looks ahead at planned steering to reduce sudden corrections, so the wheel moves more smoothly through turns. Works with Self-Tune and custom tuning. Thanks to @twilsonco for the implementation.",
"visibility": [
{
"type": "not",
"condition": {
"type": "capability",
"field": "steer_control_type",
"equals": "angle"
}
}
],
"enablement": [
{
"type": "offroad_only"
},
{
"type": "param",
"key": "NeuralNetworkLateralControl",
"equals": false
}
]
},
{ {
"key": "LiveTorqueParamsToggle", "key": "LiveTorqueParamsToggle",
"widget": "toggle", "widget": "toggle",
@@ -519,6 +545,12 @@
} }
] ]
}, },
{
"key": "RoadEdgeLaneChangeEnabled",
"widget": "toggle",
"title": "Block Lane Change: Road Edge Detection",
"description": "Blocks lane change when the model sees a road edge on the side you signal."
},
{ {
"key": "AutoLaneChangeBsmDelay", "key": "AutoLaneChangeBsmDelay",
"widget": "toggle", "widget": "toggle",
@@ -1260,6 +1292,67 @@
"label": "2 m" "label": "2 m"
} }
] ]
},
{
"key": "ScreenSaverEnabled",
"widget": "toggle",
"title": "Screen Saver",
"description": "Show a screen saver when the device is offroad and idle, instead of turning the screen off."
},
{
"key": "ScreenSaverTimeout",
"widget": "multiple_button",
"title": "Screen Saver Duration",
"description": "How long the screen saver runs before the screen turns off.",
"options": [
{
"value": 60,
"label": "1 m"
},
{
"value": 120,
"label": "2 m"
},
{
"value": 180,
"label": "3 m"
},
{
"value": 240,
"label": "4 m"
},
{
"value": 300,
"label": "5 m"
},
{
"value": 360,
"label": "6 m"
},
{
"value": 420,
"label": "7 m"
},
{
"value": 480,
"label": "8 m"
},
{
"value": 540,
"label": "9 m"
},
{
"value": 600,
"label": "10 m"
}
],
"enablement": [
{
"type": "param",
"key": "ScreenSaverEnabled",
"equals": true
}
]
} }
] ]
} }
@@ -2037,6 +2130,11 @@
"type": "param", "type": "param",
"key": "EnforceTorqueControl", "key": "EnforceTorqueControl",
"equals": false "equals": false
},
{
"type": "param",
"key": "LateralJerkTorqueController",
"equals": false
} }
] ]
} }
@@ -2161,6 +2259,42 @@
"type": "offroad_only" "type": "offroad_only"
} }
] ]
},
{
"key": "TeslaMadsScreenButton",
"widget": "multiple_button",
"title": "MADS Screen Activation",
"description": "Use a multi-finger press on the infotainment screen to toggle MADS. This allows the use of full MADS functionality when enabled. Selecting a higher finger count may reduce accidental activations. Note: Setting this to Off will reset your MADS settings to default.",
"options": [
{
"value": 0,
"label": "Off"
},
{
"value": 1,
"label": "3-Finger"
},
{
"value": 2,
"label": "4-Finger"
},
{
"value": 3,
"label": "5-Finger"
}
],
"visibility": [
{
"type": "capability",
"field": "tesla_has_vehicle_bus",
"equals": true
}
],
"enablement": [
{
"type": "offroad_only"
}
]
} }
] ]
}, },
@@ -128,3 +128,36 @@ sections:
label: 1 m label: 1 m
- value: 120 - value: 120
label: 2 m label: 2 m
- key: ScreenSaverEnabled
widget: toggle
title: Screen Saver
description: Show a screen saver when the device is offroad and idle, instead of turning the screen off.
- key: ScreenSaverTimeout
widget: multiple_button
title: Screen Saver Duration
description: How long the screen saver runs before the screen turns off.
options:
- value: 60
label: 1 m
- value: 120
label: 2 m
- value: 180
label: 3 m
- value: 240
label: 4 m
- value: 300
label: 5 m
- value: 360
label: 6 m
- value: 420
label: 7 m
- value: 480
label: 8 m
- value: 540
label: 9 m
- value: 600
label: 10 m
enablement:
- type: param
key: ScreenSaverEnabled
equals: true
@@ -73,6 +73,9 @@ sections:
- type: param - type: param
key: EnforceTorqueControl key: EnforceTorqueControl
equals: false equals: false
- type: param
key: LateralJerkTorqueController
equals: false
- id: camera - id: camera
title: Camera title: Camera
description: Camera position and calibration description: Camera position and calibration
@@ -127,6 +127,21 @@ sections:
key: EnforceTorqueControl key: EnforceTorqueControl
equals: true equals: true
items: items:
- key: LateralJerkTorqueController
widget: toggle
title: Lateral Jerk Torque Controller
description: Looks ahead at planned steering to reduce sudden corrections, so the wheel moves more smoothly through turns. Works with Self-Tune and custom tuning. Thanks to @twilsonco for the implementation.
visibility:
- type: not
condition:
type: capability
field: steer_control_type
equals: angle
enablement:
- $ref: '#/macros/offroad'
- type: param
key: NeuralNetworkLateralControl
equals: false
- key: LiveTorqueParamsToggle - key: LiveTorqueParamsToggle
widget: toggle widget: toggle
title: Self-Tune title: Self-Tune
@@ -242,6 +257,10 @@ sections:
label: 2 seconds label: 2 seconds
- value: 5 - value: 5
label: 3 seconds label: 3 seconds
- key: RoadEdgeLaneChangeEnabled
widget: toggle
title: 'Block Lane Change: Road Edge Detection'
description: Blocks lane change when the model sees a road edge on the side you signal.
- key: AutoLaneChangeBsmDelay - key: AutoLaneChangeBsmDelay
widget: toggle widget: toggle
title: 'Auto Lane Change: Delay with Blind Spot' title: 'Auto Lane Change: Delay with Blind Spot'
@@ -56,6 +56,28 @@ sections:
title: Cooperative Steering (Beta) title: Cooperative Steering (Beta)
enablement: enablement:
- $ref: '#/macros/offroad' - $ref: '#/macros/offroad'
- key: TeslaMadsScreenButton
widget: multiple_button
title: MADS Screen Activation
description: 'Use a multi-finger press on the infotainment screen to toggle MADS.
This allows the use of full MADS functionality when enabled. Selecting a higher
finger count may reduce accidental activations. Note: Setting this to Off will
reset your MADS settings to default.'
options:
- value: 0
label: 'Off'
- value: 1
label: 3-Finger
- value: 2
label: 4-Finger
- value: 3
label: 5-Finger
visibility:
- type: capability
field: tesla_has_vehicle_bus
equals: true
enablement:
- $ref: '#/macros/offroad'
- id: toyota - id: toyota
title: Toyota / Lexus Settings title: Toyota / Lexus Settings
description: '' description: ''
@@ -257,20 +257,26 @@ class TestKnownPanels:
assert "mads_settings" in sub_ids assert "mads_settings" in sub_ids
def test_mutual_exclusion_torque_nnlc(self, schema): def test_mutual_exclusion_torque_nnlc(self, schema):
"""EnforceTorqueControl and NNLC must reference each other in enablement.""" """EnforceTorqueControl, EnhancedLatAccel, and NNLC must reference each other in enablement."""
torque = nnlc = None torque = nnlc = enhanced = None
for panel in schema["panels"]: for panel in schema["panels"]:
for item in _iter_panel_items(panel): for item in _iter_panel_items(panel):
if item["key"] == "EnforceTorqueControl": if item["key"] == "EnforceTorqueControl":
torque = item torque = item
elif item["key"] == "NeuralNetworkLateralControl": elif item["key"] == "NeuralNetworkLateralControl":
nnlc = item nnlc = item
elif item["key"] == "LateralJerkTorqueController":
enhanced = item
assert torque is not None, "EnforceTorqueControl item missing" assert torque is not None, "EnforceTorqueControl item missing"
assert nnlc is not None, "NeuralNetworkLateralControl item missing" assert nnlc is not None, "NeuralNetworkLateralControl item missing"
assert enhanced is not None, "LateralJerkTorqueController item missing"
torque_enable_keys = {r.get("key") for r in torque.get("enablement", []) if r.get("type") == "param"} torque_enable_keys = {r.get("key") for r in torque.get("enablement", []) if r.get("type") == "param"}
assert "NeuralNetworkLateralControl" in torque_enable_keys assert "NeuralNetworkLateralControl" in torque_enable_keys
nnlc_enable_keys = {r.get("key") for r in nnlc.get("enablement", []) if r.get("type") == "param"} nnlc_enable_keys = {r.get("key") for r in nnlc.get("enablement", []) if r.get("type") == "param"}
assert "EnforceTorqueControl" in nnlc_enable_keys assert "EnforceTorqueControl" in nnlc_enable_keys
assert "LateralJerkTorqueController" in nnlc_enable_keys
enhanced_enable_keys = {r.get("key") for r in enhanced.get("enablement", []) if r.get("type") == "param"}
assert "NeuralNetworkLateralControl" in enhanced_enable_keys
class TestKnownVehicleSettings: class TestKnownVehicleSettings:
@@ -17,6 +17,26 @@ ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 3, 1: 5, 2: 7, 3: 10, 4: 15, 5: 30, **{i: (
VALID_TIMER_VALUES = set(ONROAD_BRIGHTNESS_TIMER_VALUES.values()) VALID_TIMER_VALUES = set(ONROAD_BRIGHTNESS_TIMER_VALUES.values())
def _resolve_brand(_params) -> str:
bundle = _params.get("CarPlatformBundle")
if isinstance(bundle, dict) and bundle.get("brand"):
return str(bundle["brand"])
# Auto-fingerprinted cars have no bundle, fall back to the last known CarParams.
CP_bytes = _params.get("CarParamsPersistent")
if CP_bytes is None:
return ""
# Never raises: callers rely on "" to mean "brand unknown, skip the migration".
try:
from openpilot.cereal import messaging # lazy: avoids heavy import at module level
from opendbc.car.structs import car
return str(messaging.log_from_bytes(CP_bytes, car.CarParams).brand)
except Exception as e:
cloudlog.exception(f"params_migration: failed to resolve brand from CarParamsPersistent: {e}")
return ""
def _migrate_car_platform_bundle(_params): def _migrate_car_platform_bundle(_params):
bundle = _params.get("CarPlatformBundle") bundle = _params.get("CarPlatformBundle")
if bundle is None: if bundle is None:
@@ -47,6 +67,23 @@ def _migrate_car_platform_bundle(_params):
cloudlog.info(f"params_migration: CarPlatformBundle migrated {old_platform!r} -> {new_platform!r}") cloudlog.info(f"params_migration: CarPlatformBundle migrated {old_platform!r} -> {new_platform!r}")
def _migrate_tesla_mads_screen_button(_params):
# TeslaMadsScreenButton defaults to Off for fresh installs, but the screen button was previously always
# active on Teslas with a vehicle bus. Seed existing Tesla installs with 3-finger to preserve that.
try:
if _params.get("TeslaMadsScreenButton") is not None:
return
if _resolve_brand(_params) != "tesla":
return
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType # lazy: avoids heavy import at module level
_params.put("TeslaMadsScreenButton", MadsScreenButtonType.THREE_FINGER, block=True)
cloudlog.info("params_migration: seeded TeslaMadsScreenButton with 3-finger to preserve existing behavior")
except Exception as e:
cloudlog.exception(f"Error migrating TeslaMadsScreenButton: {e}")
def run_migration(_params): def run_migration(_params):
# migrate OnroadScreenOffBrightness # migrate OnroadScreenOffBrightness
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION: if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
@@ -80,3 +117,6 @@ def run_migration(_params):
cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}") cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}")
_migrate_car_platform_bundle(_params) _migrate_car_platform_bundle(_params)
# seed TeslaMadsScreenButton for existing Tesla installs
_migrate_tesla_mads_screen_button(_params)
@@ -0,0 +1,21 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import os
SUNNYPILOT_CAR_SEGMENTS_REPO = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO",
"https://huggingface.co/datasets/sunnypilot/sunnypilotCarSegments")
SUNNYPILOT_CAR_SEGMENTS_BRANCH = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_BRANCH", "main")
def get_url(route, segment, file="rlog.zst"):
return f"{SUNNYPILOT_CAR_SEGMENTS_REPO}/resolve/{SUNNYPILOT_CAR_SEGMENTS_BRANCH}/segments/{route.replace('|', '/')}/{segment}/{file}"
def sunnypilot_car_segments_source(sr, seg_idxs, fns, /):
from openpilot.tools.lib.file_sources import eval_source
return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs})
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import argparse
import os
import tempfile
import requests
from huggingface_hub import HfApi
from tqdm import tqdm
from openpilot.tools.lib.route import Route
REPO_ID = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO_ID", "sunnypilot/sunnypilotCarSegments")
def upload_route(route_name: str, dry_run: bool = False) -> None:
route = Route(route_name)
log_paths = route.log_paths()
valid_segments = [(i, url) for i, url in enumerate(log_paths) if url is not None]
print(f"Route: {route_name}")
print(f"Segments: {len(valid_segments)}/{len(log_paths)}")
if not valid_segments:
print("No segments found.")
return
api = HfApi()
with tempfile.TemporaryDirectory() as tmpdir:
for seg_idx, url in tqdm(valid_segments, desc="Uploading"):
filename = url.split("?")[0].rsplit("/", 1)[-1]
local_path = os.path.join(tmpdir, f"{seg_idx}_{filename}")
resp = requests.get(url, stream=True)
resp.raise_for_status()
with open(local_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
repo_path = f"segments/{route_name.replace('|', '/')}/{seg_idx}/{filename}"
if dry_run:
size_mb = os.path.getsize(local_path) / 1024 / 1024
print(f" [{seg_idx}] {size_mb:.1f} MB -> {repo_path}")
else:
api.upload_file(
path_or_fileobj=local_path,
path_in_repo=repo_path,
repo_id=REPO_ID,
repo_type="dataset",
)
print("Done.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Upload route rlogs to sunnypilot HuggingFace dataset")
parser.add_argument("route", help="Route ID (e.g. 5beb9b58bd12b691/0000010a--a51155e496)")
parser.add_argument("--dry-run", action="store_true", help="Download and show sizes without uploading")
args = parser.parse_args()
upload_route(args.route, dry_run=args.dry_run)
+1 -1
View File
@@ -88,7 +88,7 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool:
return use_sunnylink_uploader(params) return use_sunnylink_uploader(params)
def is_tinygrad_model(started, params, CP: car.CarParams) -> bool: def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is SNPE.""" """Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad) return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad)
def is_stock_model(started, params, CP: car.CarParams) -> bool: def is_stock_model(started, params, CP: car.CarParams) -> bool:
@@ -0,0 +1,118 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import os
import time
import pyray as rl
from openpilot.common.hardware import HARDWARE
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
class ScreenSaverSP(Widget):
def __init__(self, params: Params | None = None):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._params = params or Params()
self._is_mici = HARDWARE.get_device_type() == 'mici' or (HARDWARE.get_device_type() == "pc" and os.getenv("BIG") != "1")
self.x = 0.0
self.y = 100.0
self.vx = 120.0 if self._is_mici else 300.0
self.vy = 70.0 if self._is_mici else 200.0
self._hue = 150
self.color = rl.color_from_hsv(self._hue, 1, 1)
self.text = "sunnypilot"
self.font_size = 50 if self._is_mici else 200
self._start_time = None
self._dismiss = False
self._screensaver_timeout = 300
self._hit_last_frame = False
@property
def is_active(self) -> bool:
return self._start_time is not None and not self._dismiss
@property
def was_dismissed(self) -> bool:
return self._dismiss
def initialize(self):
self._screensaver_timeout = self._params.get("ScreenSaverTimeout", return_default=True)
if self._start_time is None:
self._start_time = time.monotonic()
self._dismiss = False
def hide_event(self):
super().hide_event()
self._dismiss = False
self._start_time = None
def _handle_mouse_release(self, mouse_pos):
self._dismiss = True
self._start_time = None
gui_app.pop_widget()
return super()._handle_mouse_release(mouse_pos)
def _update_state(self):
super()._update_state()
self.font = gui_app.font(FontWeight.AUDIOWIDE)
text_size = measure_text_cached(self.font, self.text, self.font_size, 0)
self.logo_width = text_size.x
self.logo_height = text_size.y
if self._start_time and time.monotonic() - self._start_time > self._screensaver_timeout:
self._dismiss = True
self._start_time = None
dt = rl.get_frame_time()
self.x += self.vx * dt
self.y += self.vy * dt
hit_x = hit_y = False
if self.x + self.logo_width > self.rect.width:
self.vx *= -1
self.x = self.rect.width - self.logo_width
hit_x = True
elif self.x < 0:
self.vx *= -1
self.x = 0
hit_x = True
if self.y + self.logo_height > self.rect.height:
self.vy *= -1
self.y = self.rect.height - self.logo_height
hit_y = True
elif self.y < 0:
self.vy *= -1
self.y = 0
hit_y = True
hit = hit_x or hit_y
if hit and not self._hit_last_frame:
while self._hue_dist((new_hue := rl.get_random_value(0, 360)), self._hue) < 120:
pass
self._hue = new_hue
self.color = rl.color_from_hsv(self._hue, 1, 1)
self._hit_last_frame = hit
@staticmethod
def _hue_dist(a, b):
d = abs(a - b)
return min(d, 360 - d)
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
rl.clear_background(rl.BLACK)
rl.draw_text_ex(self.font, self.text, rl.Vector2(int(self.x), int(self.y)), self.font_size, 0, self.color)
return -1
+3 -1
View File
@@ -22,6 +22,8 @@ from openpilot.tools.lib.file_sources import comma_api_source, internal_source,
from openpilot.tools.lib.route import SegmentRange, FileName from openpilot.tools.lib.route import SegmentRange, FileName
from openpilot.tools.lib.log_time_series import msgs_to_time_series from openpilot.tools.lib.log_time_series import msgs_to_time_series
from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source
LogMessage = type[capnp._DynamicStructReader] LogMessage = type[capnp._DynamicStructReader]
LogIterable = Iterable[LogMessage] LogIterable = Iterable[LogMessage]
RawLogIterable = Iterable[bytes] RawLogIterable = Iterable[bytes]
@@ -246,7 +248,7 @@ class LogReader:
def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG, def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG,
sources: list[Source] | None = None, sort_by_time=False, only_union_types=False): sources: list[Source] | None = None, sort_by_time=False, only_union_types=False):
if sources is None: if sources is None:
sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source] sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source, sunnypilot_car_segments_source]
self.default_mode = default_mode self.default_mode = default_mode
self.sources = sources self.sources = sources
+1
View File
@@ -75,6 +75,7 @@ testing = [
] ]
dev = [ dev = [
"huggingface_hub",
"matplotlib", "matplotlib",
] ]
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR=$(dirname "$0")
OPENPILOT_DIR=$SCRIPT_DIR/../../
DOCKER_IMAGE=sunnypilot
DOCKER_FILE=Dockerfile.openpilot
DOCKER_REGISTRY=ghcr.io/sunnypilot
COMMIT_SHA=$(git rev-parse HEAD)
if [ -n "$TARGET_ARCHITECTURE" ]; then
PLATFORM="linux/$TARGET_ARCHITECTURE"
TAG_SUFFIX="-$TARGET_ARCHITECTURE"
else
PLATFORM="linux/$(uname -m)"
TAG_SUFFIX=""
fi
LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX
REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG
REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA
DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR
if [ -n "$PUSH_IMAGE" ]; then
docker push $REMOTE_TAG
docker tag $REMOTE_TAG $REMOTE_SHA_TAG
docker push $REMOTE_SHA_TAG
fi
+38 -109
View File
@@ -5,8 +5,6 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
import os
import pickle
import sys import sys
import hashlib import hashlib
import json import json
@@ -14,46 +12,8 @@ import re
from pathlib import Path from pathlib import Path
from datetime import datetime, UTC from datetime import datetime, UTC
REQUIRED_OUTPUT_KEYS = frozenset({
"plan",
"lane_lines",
"road_edges",
"lead",
"desire_state",
"desire_pred",
"meta",
"lead_prob",
"lane_lines_prob",
"pose",
"wide_from_device_euler",
"road_transform",
"hidden_state",
})
OPTIONAL_OUTPUT_KEYS = frozenset({
"planplus",
"sim_pose",
"desired_curvature",
})
def create_short_name(full_name: str) -> str:
def validate_model_outputs(metadata_paths: list[Path]) -> None:
combined_keys: set[str] = set()
for path in metadata_paths:
if path.stat().st_size == 0:
print(f"skipping empty metadata: {path}")
continue
with open(path, "rb") as f:
metadata = pickle.load(f)
combined_keys.update(metadata.get("output_slices", {}).keys())
missing = REQUIRED_OUTPUT_KEYS - combined_keys
if missing:
raise ValueError(f"Combined model metadata is missing required output keys: {sorted(missing)}")
detected_optional = sorted(OPTIONAL_OUTPUT_KEYS & combined_keys)
if detected_optional:
print(f"Optional output keys detected: {detected_optional}")
def create_short_name(full_name):
# Remove parentheses and extract alphanumeric words # Remove parentheses and extract alphanumeric words
clean_name = re.sub(r'\([^)]*\)', '', full_name) clean_name = re.sub(r'\([^)]*\)', '', full_name)
words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)] words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)]
@@ -121,49 +81,41 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path:
return old_pkl.rename(new_pkl) return old_pkl.rename(new_pkl)
def generate_metadata(model_path: Path, output_dir: Path, short_name: str, driving_pkl: Path): def generate_chunked_model(driving_pkl: Path) -> dict:
base = model_path.stem
metadata_file = output_dir / f"{base}_metadata.pkl"
if short_name:
renamed_meta = output_dir / f"{base}_{short_name.lower()}_metadata.pkl"
if metadata_file.exists() and not renamed_meta.exists():
metadata_file = metadata_file.rename(renamed_meta)
elif renamed_meta.exists():
metadata_file = renamed_meta
if not metadata_file.exists():
print(f"Warning: Missing metadata for {base} ({metadata_file}), skipping", file=sys.stderr)
return
tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest() tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest()
with open(metadata_file, 'rb') as f: chunks_config = []
metadata_hash = hashlib.sha256(f.read()).hexdigest() manifest_file = Path(f"{driving_pkl}.chunkmanifest")
if manifest_file.exists():
num_chunks = int(manifest_file.read_text().strip())
for i in range(num_chunks):
chunk_path = Path(f"{driving_pkl}.chunk{i + 1:02d}of{num_chunks:02d}")
if chunk_path.exists():
chunk_hash = hashlib.sha256(chunk_path.read_bytes()).hexdigest()
chunks_config.append({
"file_name": chunk_path.name,
"sha256": chunk_hash
})
model_type = "offPolicy" if "off_policy" in base else "onPolicy" if "on_policy" in base else base.split("_")[-1] artifact_data = {
"file_name": driving_pkl.name,
return { "download_uri": {
"type": model_type, "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/",
"artifact": { "sha256": tinygrad_hash
"file_name": driving_pkl.name,
"download_uri": {
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/",
"sha256": tinygrad_hash
}
},
"metadata": {
"file_name": metadata_file.name,
"download_uri": {
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/",
"sha256": metadata_hash
}
} }
} }
if chunks_config:
artifact_data["chunks"] = chunks_config
def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown"): return {
metadata_json = { "type": "chunked",
"artifact": artifact_data,
}
def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown") -> None:
bundle_json = {
"short_name": short_name, "short_name": short_name,
"display_name": custom_name or upstream_branch, "display_name": custom_name or upstream_branch,
"is_20hz": is_20hz, "is_20hz": is_20hz,
@@ -179,40 +131,26 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short
} }
# Write metadata to output_dir # Write metadata to output_dir
metadata_json = {
"bundles": [bundle_json]
}
with open(output_dir / "metadata.json", "w") as f: with open(output_dir / "metadata.json", "w") as f:
json.dump(metadata_json, f, indent=2) json.dump(metadata_json, f, indent=2)
print("Generated metadata.json")
print(f"Generated metadata.json with {len(models)} models.")
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
import glob
parser = argparse.ArgumentParser(description="Generate metadata for model files") parser = argparse.ArgumentParser(description="Generate metadata JSON for the compiled JIT model")
parser.add_argument("--model-dir", default="./models", help="Directory containing ONNX model files") parser.add_argument("--model-dir", default="./models", help="Directory containing the model files")
parser.add_argument("--output-dir", default="./output", help="Output directory for metadata") parser.add_argument("--output-dir", default="./output", help="Output directory for metadata")
parser.add_argument("--custom-name", help="Custom display name for the model") parser.add_argument("--custom-name", help="Custom display name for the model")
parser.add_argument("--is-20hz", action="store_true", help="Whether this is a 20Hz model") parser.add_argument("--is-20hz", action="store_true", help="Whether this is a 20Hz model")
parser.add_argument("--validate-only", action="store_true")
parser.add_argument("--upstream-branch", default="unknown", help="Upstream branch name") parser.add_argument("--upstream-branch", default="unknown", help="Upstream branch name")
args = parser.parse_args() args = parser.parse_args()
if args.validate_only:
metadata_paths = glob.glob(os.path.join(args.model_dir, "*_metadata.pkl"))
if not metadata_paths:
print(f"No metadata files found in {args.model_dir}", file=sys.stderr)
sys.exit(1)
validate_model_outputs([Path(p) for p in metadata_paths])
print(f"Validated {len(metadata_paths)} metadata files successfully.")
sys.exit(0)
# Find all ONNX files in the given directory
model_paths = glob.glob(os.path.join(args.model_dir, "*.onnx"))
if not model_paths:
print(f"No ONNX files found in {args.model_dir}", file=sys.stderr)
sys.exit(1)
_output_dir = Path(args.output_dir) _output_dir = Path(args.output_dir)
_output_dir.mkdir(exist_ok=True, parents=True) _output_dir.mkdir(exist_ok=True, parents=True)
_short_name = create_short_name(args.custom_name) if args.custom_name else None _short_name = create_short_name(args.custom_name) if args.custom_name else None
@@ -229,14 +167,5 @@ if __name__ == "__main__":
else: else:
_driving_pkl = new_pkl _driving_pkl = new_pkl
_models = [] _model_metadata = generate_chunked_model(_driving_pkl)
create_metadata_json([_model_metadata], _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch)
for _model_path in model_paths:
_model_metadata = generate_metadata(Path(_model_path), _output_dir, _short_name, _driving_pkl)
if _model_metadata:
_models.append(_model_metadata)
if _models:
create_metadata_json(_models, _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch)
else:
print("No models processed.", file=sys.stderr)
Generated
+116 -1
View File
@@ -5,6 +5,19 @@ requires-python = ">=3.12.3, <3.13"
[manifest] [manifest]
overrides = [{ name = "opendbc", editable = "opendbc_repo" }] overrides = [{ name = "opendbc", editable = "opendbc_repo" }]
[[package]]
name = "anyio"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
]
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "26.1.0" version = "26.1.0"
@@ -395,6 +408,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
] ]
[[package]]
name = "filelock"
version = "3.32.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" },
]
[[package]] [[package]]
name = "fonttools" name = "fonttools"
version = "4.63.0" version = "4.63.0"
@@ -412,6 +434,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
] ]
[[package]]
name = "fsspec"
version = "2026.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hf-xet"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" },
{ url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" },
{ url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" },
{ url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" },
{ url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" },
{ url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" },
{ url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "huggingface-hub"
version = "1.27.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "filelock" },
{ name = "fsspec" },
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "httpx" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
]
[[package]] [[package]]
name = "hypothesis" name = "hypothesis"
version = "6.47.5" version = "6.47.5"
@@ -730,6 +834,7 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "huggingface-hub" },
{ name = "matplotlib" }, { name = "matplotlib" },
] ]
docs = [ docs = [
@@ -788,6 +893,7 @@ requires-dist = [
{ name = "comma-deps-zstd" }, { name = "comma-deps-zstd" },
{ name = "coverage", marker = "extra == 'testing'" }, { name = "coverage", marker = "extra == 'testing'" },
{ name = "cython" }, { name = "cython" },
{ name = "huggingface-hub", marker = "extra == 'dev'" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
{ name = "inputs" }, { name = "inputs" },
{ name = "jeepney" }, { name = "jeepney" },
@@ -1302,7 +1408,7 @@ provides-extras = ["dev"]
[[package]] [[package]]
name = "tinygrad" name = "tinygrad"
version = "0.12.0" version = "0.13.0"
source = { editable = "tinygrad_repo" } source = { editable = "tinygrad_repo" }
[package.metadata] [package.metadata]
@@ -1413,6 +1519,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" },
] ]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.7.0" version = "2.7.0"