Compare commits

..

64 Commits

Author SHA1 Message Date
discountchubbs
456e7e2e15 try this one 2026-04-26 19:33:20 -07:00
discountchubbs
d38d68251b tg 2026-04-26 19:23:01 -07:00
discountchubbs
b92588de41 tg 2026-04-26 19:20:29 -07:00
discountchubbs
0ce37844b9 Merge branch 'tinygrad-sync-4/25' into sync-20260426 2026-04-26 19:20:05 -07:00
discountchubbs
db216f5c8b Revert "use catch2 dependency package (#37910)"
This reverts commit d1e069210f.
2026-04-26 19:19:46 -07:00
discountchubbs
271ed5e091 dm 2026-04-26 13:03:04 -07:00
discountchubbs
41dea5d48d Revert "sync dmonitoring too"
This reverts commit dc11e5fd84.
2026-04-26 13:02:31 -07:00
discountchubbs
dc11e5fd84 sync dmonitoring too 2026-04-26 12:53:45 -07:00
discountchubbs
ced4a664cc use upstreams 2026-04-26 11:18:51 -07:00
discountchubbs
03db277c22 dev 2026-04-26 11:15:34 -07:00
discountchubbs
11ed3800bf ugh 2026-04-26 11:10:17 -07:00
discountchubbs
92526b878c json v17 2026-04-26 11:07:17 -07:00
Jason Wen
fa85153fad Merge branch 'upstream/master' into sync-20260426
# Conflicts:
#	opendbc_repo
#	panda
#	selfdrive/monitoring/helpers.py
#	selfdrive/monitoring/test_monitoring.py
#	selfdrive/selfdrived/selfdrived.py
#	selfdrive/ui/ui_state.py
2026-04-26 03:37:06 -04:00
discountchubbs
66ff8ae52c shebang 2026-04-26 00:33:45 -07:00
discountchubbs
d85cb76304 lint 2026-04-26 00:32:31 -07:00
James Vecellio-Grant
b4c613680e Update SConscript 2026-04-26 00:29:30 -07:00
discountchubbs
f7511491f7 sync new modeld changes 2026-04-26 00:27:47 -07:00
discountchubbs
88b30e199b fix to new tg 2026-04-25 23:47:26 -07:00
discountchubbs
2898f394dd bump 2026-04-25 23:38:31 -07:00
discountchubbs
554cf9ca4a modeld: sync tinygrad 2026-04-25 23:28:02 -07:00
Jason Wen
8745cd0f38 Revert "modeld: single jit (#37758)"
This reverts commit d81d66193f.
2026-04-26 02:24:19 -04:00
Jason Wen
642421bc9b Revert "modeld: group npy -> qcom copies to avoid graph breaks (#37866)"
This reverts commit 859bd215bf.
2026-04-26 02:23:25 -04:00
Jason Wen
ddddf6231b Revert "modeld: standalone compile script (#37851)"
This reverts commit 551e2f77bf.
2026-04-26 02:22:58 -04:00
Jason Wen
12529e8d18 Revert "dmonitoringmodeld: get frame size from vipc (#37897)"
This reverts commit c3b0f0d11a.
2026-04-26 02:22:55 -04:00
Adeeb Shihadeh
d1e069210f use catch2 dependency package (#37910) 2026-04-25 13:56:25 -07:00
Shane Smiskol
ee54e82090 bump opendbc (#37907) 2026-04-24 17:56:31 -07:00
Adeeb Shihadeh
79cd8420eb jp: 2x faster parsing (#37904)
* jp: 2x faster parsing

* rm dynamic path

* cleanup

* lil more

* livin in the future

* clean that up

* one more
2026-04-24 15:02:37 -07:00
Daniel Koepping
8c533b14c0 AGNOS 18.1 (#37895)
* test agnos18.1 in staging

* loggerd: link va/va-drm/drm on larch64

* agnos 18.1 production
2026-04-24 13:24:29 -07:00
Daniel Koepping
494eba5961 Raise mici thermal limits (#37891)
* adjust thermal bands

* raise OFFROAD_DANGER_TEMP

* rename thermal bands

* rm warm
2026-04-24 13:22:43 -07:00
ZwX1616
ad875632ac camerad: switch on-sensor binning to BPS downscaling (#37876)
* update blob

* fixed

* didnt catch this

* add back

* needs BLC built in

* for real

* attempt2

* clean up override

* this should keep ox as was

* disable for OX

* update descs

---------

Co-authored-by: Comma Device <device@comma.ai>
2026-04-23 16:55:37 -07:00
Daniel Koepping
63068481d7 fix build docs CI (#37899)
docs: fix build
2026-04-23 16:19:30 -07:00
Daniel Koepping
275206c14d increase MAX_ROLL threshold for lateral_maneuvers (#37898)
increase MAX_ROLL for starting lateral maneuvers due to device mounting variance
2026-04-23 15:44:09 -07:00
Armand du Parc Locmaria
c3b0f0d11a dmonitoringmodeld: get frame size from vipc (#37897) 2026-04-23 15:23:31 -07:00
Adeeb Shihadeh
1c69770c53 tools/setup: support all common Linux distros (#37765)
* shorter ubuntu

* tools/setup: support all common Linux distros
2026-04-23 13:15:08 -07:00
Armand du Parc Locmaria
551e2f77bf modeld: standalone compile script (#37851)
* modeld: standalone compile script

* cleanup

* frame skip

* rm last op import

* dm warp

* no graph break

* +x compile_dm_warp.py

* don't import tg before setting device

* compile_modeld exports metadata

* update help

* namedtuple

* lint

* Revert "compile_modeld exports metadata"

This reverts commit 93c3c223567b4d4a074c9071d7f734c56f5aedcc.

* import
2026-04-23 11:55:07 -07:00
Andi Radulescu
ad04c6a038 cruise: fix test_cruise_speed assertion (#37802) 2026-04-23 11:52:12 -07:00
Adeeb Shihadeh
bb4b96e05d qcomgpsd: rm XTRA assistance (#37893)
* qcomgpsd: rm XTRA assistance

* lil more

* lil more
2026-04-23 10:20:09 -07:00
Adeeb Shihadeh
7d71354fd0 ui: remove firehose count (#37886) 2026-04-22 19:57:59 -07:00
Daniel Koepping
49685fc2bc ui: fix long maneuver toggle (#37622) 2026-04-22 17:59:34 -07:00
Adeeb Shihadeh
0eacf34e15 sensord: add note about shared IRQ 2026-04-22 16:49:29 -07:00
Adeeb Shihadeh
0be0d7fa94 add that back, it's used in a test 2026-04-22 16:40:42 -07:00
Adeeb Shihadeh
736cf6d9df clean up deprecated services (#37885)
* clean up deprecated services

* lil more
2026-04-22 16:01:35 -07:00
Adeeb Shihadeh
df6d34e52b remove enhancement issue template 2026-04-22 15:09:56 -07:00
Shane Smiskol
39d1eec575 Fix Tesla route spam (#37884)
bump
2026-04-22 15:05:07 -07:00
Adeeb Shihadeh
2266a9dd9c sensord: clean up SensorEventData struct (#37883) 2026-04-22 13:13:39 -07:00
Adeeb Shihadeh
f8372ccc4d sensord: remove mmc5603nj support (#37881)
* sensord: remove mmc5603nj support

* lil more

* lil more
2026-04-22 12:53:07 -07:00
Trey Moen
f8c45d307c esim: skip listing profiles on mutation ops (#37878) 2026-04-22 08:48:02 -07:00
ZwX1616
ca04b70d0a camerad: driver camera BPS magic (#37873)
* update blob

* fixed

* didnt catch this

* add back

* needs BLC built in

* for real

---------

Co-authored-by: Comma Device <device@comma.ai>
2026-04-21 21:04:06 -07:00
ZwX1616
571a547671 Fix driver preview alert text and sound (#37875)
* fix type and add text

* short

* fix sound too

---------

Co-authored-by: Comma Device <device@comma.ai>
2026-04-21 16:19:20 -07:00
Adeeb Shihadeh
b29d0a17af DM: readability, part 1 (#37872)
* spellings

* unused

* no roll

* lil more

* lil more

* one more

* policy enum

* better trans

* set_timer -> set_policy

* set_timer -> set_policy

* no yaonet

* del redundant code

---------

Co-authored-by: ZwX1616 <zwx1616@gmail.com>
2026-04-21 15:43:13 -07:00
Armand du Parc Locmaria
859bd215bf modeld: group npy -> qcom copies to avoid graph breaks (#37866)
* modeld: group npy -> qcom copies to avoid graph breaks

* batch realize

* dm as well
2026-04-21 13:50:20 -07:00
Harald Schäfer
4988a62b31 Revert "POP model (#37727)" (#37871)
This reverts commit 12f1be19cc.
2026-04-21 11:20:33 -07:00
Adeeb Shihadeh
e202bbe4aa monitoring: remove redundant README 2026-04-21 11:04:46 -07:00
Adeeb Shihadeh
4286a64083 jp: reduce y padding 2026-04-21 10:11:10 -07:00
Adeeb Shihadeh
341786acb5 jp: fix hidden plots unhiding on interaction (#37870) 2026-04-21 09:53:17 -07:00
Adeeb Shihadeh
04b23ff849 model replay: relax driverState timing (#37868) 2026-04-20 21:13:07 -07:00
Adeeb Shihadeh
6996e87f8d dm: helpers.py -> policy.py (#37864) 2026-04-20 19:20:48 -07:00
probablyanasian
b6432e705d Fix LSM6DS3 sensors test (#37855)
* fix temperature test + add std dev test for temp

* loosen gyro limit, no axis on temp mean

* whitespace fix

* add std_max to all sensors/tests
2026-04-18 20:27:58 -07:00
stef
5d7155fdda body ui c3 & c4 (#37794)
* c4 body ui

* clean up diff

* clean up

* default bodyview debug with True

* remove battery indicator and fix close settings bug

* organize debug file

* clean

* clean up frame index

* remove unneccessary is body check

* update bodyview

* remove joystick_debug_mode based events

* remove debug script

* apply suggestions

* clean diff

* clean diff

* move ignition message

* save a line

* remove visibility set and fix tici body face when sidebar open

* remove explicit textColor offroad message

* remove unused imports

* revert pairing dialog

* apply suggestions

* add body specific icon

* add body icon for homescreen

* set mode for state on tici after on body changed

* tiny

* tweak

* v

* tweaks

* icon ratio was wrong!

* same order

* rm

* apply suggestions

* remove commented lines in animation

* onroad click callback was on home bug and fix setup widget settings call back hack

* fix body changed

* one liner

* clean up

* formatting

* if false

* revert to master + reimplement

* close sidebar on body home tici

* make ignition message bigger on c3

* flip eye direction when turning

---------

Co-authored-by: Nick <nickorie@gmail.com>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
2026-04-18 13:00:05 -07:00
Trey Moen
b9986cae06 lpa: add is_euicc() (#37847) 2026-04-18 12:38:21 -07:00
Jason Wen
2c0903e45e tools: add retry mechanism for API requests (#36617) 2026-04-18 12:21:47 -07:00
ZwX1616
389b639ef2 DriverMonitoringState v2 (#37799)
* draft ds

* better names

* what is this

* build new

* better names2

* more

* bit more cleanup

* rm those

* .

* .2

* selfdrived

* depre

* hk

* fix test

* fix rest

* 1

* fix enum

* update cereal

* fix rest

* more

* add step

* fix all

* imports

* cant?

* .

* simplify

* bool

* fix some migrate

* cleanup

* fix fix

* Update cereal/log.capnp

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>

* touchup

* what

---------

Co-authored-by: Comma Device <device@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-04-17 21:58:05 -07:00
stef
5624a4ccd6 bump teleoprtc_repo (#37848) 2026-04-17 15:41:47 -07:00
Armand du Parc Locmaria
d81d66193f modeld: single jit (#37758)
* compile_modeld.py

* update estimates

* missing image=2?

* Revert "missing image=2?"

This reverts commit 2f5952eb63ba1e3f24cbf5769e6b5e9170d7f0a6.

* Revert "update estimates"

This reverts commit 1f72feef2ffdec6126e3c941e899b46ace7b4b65.

* Revert "compile_modeld.py"

This reverts commit f10541502efca02725f368deda2a21d1f786f57d.

* load warp in ModelState init

* dead code

* prep

* compile modeld

* update SConscript

* tmp save plot locally

* Revert "tmp save plot locally"

This reverts commit ec22f15161ad3b0241a097546b35860f989219f5.

* openpilot hacks?

* no float16

* tmp more chunks

* Revert "tmp more chunks"

This reverts commit 9e1d9b4d0dc36ff530d2a70b565fbfabd7afb00d.

* Revert "no float16"

This reverts commit 6204956e98e3c0818ed1985ede8eeccb810f63e3.

* realize boundaries

* Revert "realize boundaries"

This reverts commit ffaa19259eba70944e7793e8f51a0f87089531b3.

* prune=False?

* Reapply "tmp more chunks"

This reverts commit 2599c41cea93b4a6b4e946cdffc6a617663a7d23.

* tg bug?

* load first?

* Revert "load first?"

This reverts commit f643d082d76a424b23295e254179eb111e936e61.

* revert

* Reapply "tmp save plot locally"

This reverts commit 1b95b82ee58654bd908b1cb04ab0ddbcd1a5955d.

* 0 tol pc

* warp -> modeld

* rename

* bypass chunking?

* dont chunk

* Revert "dont chunk"

This reverts commit cc97fc67b3203456e123f02babe5c83b87c7e264.

* dont chunk

* debug

* Revert "debug"

This reverts commit b3c2f2e7a095fd32f8d8562a68fd1cca42357eac.

* Revert "dont chunk"

This reverts commit 42bd9b6f6ad0722c50348ba11ba7e2a64fdf997d.

* Revert "bypass chunking?"

This reverts commit ad5422a93483ffd8a59ba62e5fb72ced3b5d04d0.

* corrupt model outputs

* Revert "corrupt model outputs"

This reverts commit 245feb94480e02f83a20b65a9488652bcbfc88b0.

* image=0 for warp, match master

* dedupe enqueue

* pass traffic convention

* tg buffer for desire

* dedupe buffer creation

* compile_modeld: nuke stale cached pkl before compiling

The UNSAFE CI checkout keeps gitignored files (.pkl, .sconsign.dblite),
so stale pkl files from previous commits can persist and be reused
instead of being recompiled. Delete them explicitly before compiling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test vs compile

* all outputs need to be different on different inputs

* randomize numpy inputs

* randomize on every step

* SConscript: nuke stale pkl+chunks before compile_modeld

Move the stale artifact cleanup from compile_modeld.py into the
SConscript build command. This ensures stale gitignored pkl and chunk
files are deleted even if scons decides to skip the compile step
(due to a stale .sconsign.dblite from UNSAFE CI checkout).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* compile_modeld: restore Context(IMAGE=0) for warp

The warp operations must run under IMAGE=0 to avoid QCOM image texture
optimizations that corrupt the output buffer after ~33 frames.
This was accidentally commented out in a855173.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* modeld: create SubMaster before model loading

Move PubMaster/SubMaster creation before the model loading step.
During model loading (3.5s+), process_replay may send liveCalibration.
If SubMaster doesn't exist yet, the message is dropped and the warp
transform stays as zeros, producing garbage warped images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "modeld: create SubMaster before model loading"

This reverts commit 968c987c2fbb3fce141c4e345d10ddea559b6c50.

* stale metadata?

* claude debug

* Revert "claude debug"

This reverts commit 49e754c6affa45a8ea8834588a00227b8090b17a.

* Revert "stale metadata?"

This reverts commit 870388513c0d4a67dcf970cd277b6db56cb2b478.

* modeld: realize jit outputs before parsing

* Update modeld.py

* modeld: fix NameError by removing redundant MODELS_DIR definition

* test buffers in test vs. compile

* 2x inputs before running

* fixup 2x inputs test

* realize onnx weights?

* Revert "realize onnx weights?"

This reverts commit 49c8b9a505db38ff22f342db011a3a6b6526d398.

* move openpilot_hacks flag to sconscript

* stricter test vs compile

* correct timings

* more run more fail?

* Revert "more run more fail?"

This reverts commit 9e94bb63940751ec29e81b634c42449113e1f2e5.

* numpy shenanigans

* correct shapes

* dont assert timings for now

* Revert "correct shapes"

This reverts commit 5b9ff6c84c0022327d21801d179e9e51c39e8f78.

* Revert "numpy shenanigans"

This reverts commit b4f6fb3078d7e9b09698895b88728fd8eea8c8a8.

* no need to nuke

* comment unused

* don't use NPY device

* copy instead of from_blob

* to device before jit

* Revert "to device before jit"

This reverts commit 7a59ed9b1ac88657b5a3917986b6ff92e59a2ee3.

* Revert "copy instead of from_blob"

This reverts commit 196c4892a06ffba89ef631876372cecf137cc1b4.

* Revert "don't use NPY device"

This reverts commit 18abf43bbac46ad47a60c03dd8d1ef40b3f59227.

* 3 runs is enough

* no_memory_planner=1

* lint

* restore model_replay.py

* on policy -> policy

* unused

* prepare only enqueues full images

* warp with image=2?

* unused args

* test vs compile, check different inputs different outputs

* avoid uop cache collision

* dont need realize here

* misc

* input queues diverged

* strict zip

* monkey patch for now

* memory planner

* prev desire correct order

* dedupe pkl paths / compile targets

* don't change behavior, warp and enqueue frames when skipping model eval

* actually prepare only

* warm up warp jit

* correct path

* oops

* explicit warmup

* need continuous + can't have dupplicate jit inputs

* whitespace

* bufs -> input_queues

* master tg

* /N_RUNS

* bump tg, remove uop cache patch

* more readable

* Revert "bump tg, remove uop cache patch"

This reverts commit 499acca2591becd389de4025943f9e776a5b337c.

* missing dep

---------

Co-authored-by: Bruce Wayne <harald.the.engineer@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 12:37:56 -07:00
157 changed files with 2756 additions and 10118 deletions

View File

@@ -1,8 +0,0 @@
---
name: Enhancement
about: For openpilot enhancement suggestions
title: ''
labels: 'enhancement'
assignees: ''
---

View File

@@ -23,43 +23,56 @@ env:
CI: 1
jobs:
generate_cereal_artifact:
name: Generate cereal validation artifacts
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
submodules: true
- run: ./tools/op.sh setup
- name: Build openpilot
run: scons -j$(nproc) cereal
- name: Dump sunnypilot schema
run: |
export PYTHONPATH=${{ github.workspace }}
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema.json
- name: 'Prepare artifact'
run: |
mkdir -p "cereal/messaging/tests/cereal_validations"
cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py"
cp schema.json "cereal/messaging/tests/cereal_validations/schema.json"
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
name: cereal_validations
path: cereal/messaging/tests/cereal_validations
validate_cereal_with_upstream:
name: Validate cereal with Upstream
runs-on: ubuntu-24.04
needs: generate_cereal_artifact
steps:
- name: Checkout sunnypilot cereal
- name: Checkout sunnypilot
uses: actions/checkout@v6
with:
sparse-checkout: cereal
- name: Init sunnypilot opendbc submodule
run: git submodule update --init --depth 1 opendbc_repo
- name: Checkout upstream openpilot cereal
- name: Checkout upstream openpilot
uses: actions/checkout@v6
with:
repository: 'commaai/openpilot'
path: upstream_openpilot
sparse-checkout: cereal
path: openpilot
submodules: true
ref: "refs/heads/master"
- name: Init upstream opendbc submodule
working-directory: upstream_openpilot
run: git submodule update --init --depth 1 opendbc_repo
- name: Install uv
run: pip install uv
- name: Generate sunnypilot schema
- run: ./tools/op.sh setup
- name: Build openpilot
working-directory: openpilot
run: scons -j$(nproc) cereal
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: cereal_validations
path: openpilot/cereal/messaging/tests/cereal_validations
- name: 'Validate sunnypilot schema against upstream'
run: |
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
-g -f /tmp/sp_schema.json --cereal-dir cereal
- name: Validate against upstream
run: |
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
-r -f /tmp/sp_schema.json --cereal-dir upstream_openpilot/cereal
export PYTHONPATH=${{ github.workspace }}/openpilot
chmod +x openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py
python3 openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f openpilot/cereal/messaging/tests/cereal_validations/schema.json

View File

@@ -172,8 +172,8 @@ jobs:
output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl"
echo "Compiling: $onnx_file -> $output_file"
QCOM=1 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1 IMAGE=2 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done
- name: Validate Model Outputs

View File

@@ -1,4 +1,4 @@
sunnypilot Version 2026.001.000 (2026-05-06)
sunnypilot Version 2026.001.000 (2026-03-xx)
========================
* What's Changed (sunnypilot/sunnypilot)
* Complete rewrite of the user interface from Qt C++ to Raylib Python
@@ -66,64 +66,6 @@ sunnypilot Version 2026.001.000 (2026-05-06)
* Pause Lateral Control with Blinker: Post-Blinker Delay by @CHaucke89
* SCC-V: Use p97 for predicted lateral accel by @yasu-oh
* Controls: Support for Torque Lateral Control v0 Tune by @sunnyhaibin
* [TIZI/TICI] ui: ensure null checks for `CarParams` and `CarParamsSP` by @sunnyhaibin
* [TIZI/TICI] ui: use `vCruiseCluster` and `vEgoCluster` for SLA `preActive` by @sunnyhaibin
* Fix display of values when using use_float_scaling by @CHaucke89
* models: fix default & index "0" by @nayan8teen
* [TIZI/TICI] visuals: Improved speed limit by @angaz
* ICBM: ensure button timers update on disable to clear stale presses by @jamesmikesell
* [TIZI/TICI] ui: simplify Smart Cruise Control text rendering by @sunnyhaibin
* controlsd: fix steer_limited_by_safety not updating under MADS by @zephleggett
* soundd: trigger timeout warning during MADS lateral-only by @zephleggett
* pandad: flasher for Rivian long upgrade module by @lukasloetkolben
* modeld_v2: tinygrad transformation warp by @Discountchubbs
* tools: block `manage_sunnylinkd` in sim startup script by @sunnyhaibin
* [MICI] ui: need superclass `_render` in `HudRendererSP` by @sunnyhaibin
* [TIZI/TICI] ui: Speed Limit Assist active status by @sunnyhaibin
* ui: reimplement "Screen Off" option to Onroad Brightness by @sunnyhaibin
* ui: don't hide steering wheel when blindspot disabled by @royjr
* ui: Speed Limit Assist `preActive` improvements by @sunnyhaibin
* ui: consolidate Speed Limit Assist `preActive` status rendering by @sunnyhaibin
* [MICI] ui: Speed Limit Assist `preActive` status by @sunnyhaibin
* sunnypilot modeld: remove thneed modeld by @Discountchubbs
* modeld_v2: decouple planplus scaling from accel by @Discountchubbs
* sunnylink: Handle exceptions in `getParamsAllKeysV1` to log crashes by @devtekve
* [TIZI/TICI] ui: Developer UI cleanup by @sunnyhaibin
* [TIZI/TICI] ui: dynamic alert size by @nayan8teen
* i18n(fr): Add French translations by @didlawowo
* Toyota: Stop and Go Hack (Alpha) by @sunnyhaibin
* ui: `AlertFadeAnimator` for longitudinal-related statuses by @sunnyhaibin
* pandad: gate unsupported pandas before flashing by @sunnyhaibin
* Rivian: Flash xnor's Longitudinal Upgrade Kit prior supported panda check by @lukasloetkolben
* [TIZI/TICI] ui: add back gate steering arc behind toggle by @sunnyhaibin
* ui: gate Onroad Brightness Delay on readiness by @sunnyhaibin
* ui: add new timer options for Onroad Brightness Delay by @sunnyhaibin
* [TIZI/TICI] ui: branch switcher is always available by @sunnyhaibin
* pandad: always prioritize internal panda by @sunnyhaibin
* sunnylinkd: fetch compressed params schema by @sunnyhaibin
* sunnypilot locationd: remove unused car_ekf filter by @sunnyhaibin
* modeld_v2: update deprecated temporalPose ref by @sunnyhaibin
* NNLC: restore pre-v1 PID gains in torque extension by @mmmorks
* MADS safety: enable heartbeat and lateral controls mismatch checks by @sunnyhaibin
* [MICI] ui: models panel enhancements by @nayan8teen
* [TIZI/TICI] ui: fix unintended selection while scrolling in TreeOptionDialog by @TheSecurityDev
* tools: script for video concatenation by @Discountchubbs
* tools: profile memory usage by @Discountchubbs
* [TIZI/TICI] ui: remove per-frame param sync by @sunnyhaibin
* [MICI] ui: always offroad by @nayan8teen
* controls: always default Torque Lateral Control to v0 Tune by @sunnyhaibin
* Revert "controls: always default Torque Lateral Control to v0 Tune" by @sunnyhaibin
* Reapply "controls: always default Torque Lateral Control to v0 Tune" (#1806) by @sunnyhaibin
* [MICI] ui: add sunnylink info & connectivity check by @nayan8teen
* sunnylink: Remove unused API endpoint by @devtekve
* DM: wheel touch enforcement in MADS by @sunnyhaibin
* torque: show static override values in Dev UI & gate `useParams` on custom torque tune by @sunnyhaibin
* MADS: suppress espActive event when long is not engaged by @sunnyhaibin
* sunnylink: SDUI by @sunnyhaibin
* [MICI] ui: align upstream changes with sunnypilot settings buttons by @nayan8teen
* ui: fix cellular toggles by @AmyJeanes
* sunnylink: switch athena domain by @devtekve
* Platform List: dynamically migrate CarPlatformBundle by @sunnyhaibin
* What's Changed (sunnypilot/opendbc)
* Honda: DBC for Accord 9th Generation by @mvl-boston
* FCA: update tire stiffness values for `RAM_HD` by @dparring
@@ -142,25 +84,12 @@ sunnypilot Version 2026.001.000 (2026-05-06)
* Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston
* GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin
* GM: remove `CHEVROLET_BOLT_NON_ACC_2ND_GEN` from `dashcamOnly` by @sunnyhaibin
* Hyundai Longitudinal: deprecate ramp update for dynamic tune by @Discountchubbs
* Rivian: long upgrade messages on bus 1 by @lukasloetkolben
* Toyota: Stop and Go Hack (Alpha) by @sunnyhaibin
* Toyota: gate Smart DSU behind Alpha Longitudinal by @sunnyhaibin
* Toyota: Gas Interceptor always set `standstill_req` by @sunnyhaibin
* MADS safety: dedicated `controls_allowed_lateral` by @sunnyhaibin
* Platform List: include community supported platforms by @sunnyhaibin
* New Contributors (sunnypilot/sunnypilot)
* @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots"
* @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters"
* @Candy0707 made their first contribution in "[TIZI/TICI] ui: Fix misaligned turn signals and blindspot indicators with sidebar"
* @CHaucke89 made their first contribution in "Pause Lateral Control with Blinker: Post-Blinker Delay"
* @yasu-oh made their first contribution in "SCC-V: Use p97 for predicted lateral accel"
* @angaz made their first contribution in "[TIZI/TICI] visuals: Improved speed limit"
* @jamesmikesell made their first contribution in "ICBM: ensure button timers update on disable to clear stale presses"
* @zephleggett made their first contribution in "controlsd: fix steer_limited_by_safety not updating under MADS"
* @lukasloetkolben made their first contribution in "pandad: flasher for Rivian long upgrade module"
* @didlawowo made their first contribution in "i18n(fr): Add French translations"
* @mmmorks made their first contribution in "NNLC: restore pre-v1 PID gains in torque extension"
* New Contributors (sunnypilot/opendbc)
* @AmyJeanes made their first contribution in "Tesla: Fix stock LKAS being blocked when MADS is enabled"
* @mvl-boston made their first contribution in "Honda: Update Clarity brake to renamed DBC message name"
@@ -170,20 +99,6 @@ sunnypilot Version 2026.001.000 (2026-05-06)
* @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint"
* @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`"
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000
************************
* Synced with commaai's openpilot (v0.11.1)
* master commit c001f3c9b490a80e69539f0af6022f6e07ceb721 (April 16, 2026)
* New driver monitoring model
* Improved image processing pipeline for driver camera
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
* New driving model #36798
* Fully trained using a learned simulator
* Improved longitudinal performance in Experimental mode
* Reduce comma four standby power usage by 77% to 52 mW
* Kia K7 2017 support thanks to royjr!
* Lexus LS 2018 support thanks to Hacheoy!
* Improved inter-process communication memory efficiency
* comma four support
sunnypilot Version 2025.002.000 (2025-11-06)
========================

View File

@@ -342,7 +342,6 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitChanged @21;
speedLimitPending @22;
e2eChime @23;
laneChangeRoadEdge @24;
}
}
@@ -449,8 +448,6 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
leftLaneChangeEdgeBlock @1 :Bool;
rightLaneChangeEdgeBlock @2 :Bool;
enum TurnDirection {
none @0;

View File

@@ -273,11 +273,7 @@ struct GPSNMEAData {
nmea @2 :Text;
}
# android sensor_event_t
struct SensorEventData {
version @0 :Int32;
sensor @1 :Int32;
type @2 :Int32;
timestamp @3 :Int64;
union {
@@ -296,7 +292,10 @@ struct SensorEventData {
struct SensorVec {
v @0 :List(Float32);
status @1 :Int8;
deprecated :group {
status @1 :Int8;
}
}
enum SensorSource {
@@ -314,7 +313,11 @@ struct SensorEventData {
mmc5603nj @11;
}
# formerly based on android sensor_event_t
deprecated :group {
version @0 :Int32;
sensor @1 :Int32;
type @2 :Int32;
uncalibrated @10 :Bool;
}
}
@@ -457,10 +460,10 @@ struct DeviceState @0xa4d8b5af2aa492eb {
}
enum ThermalStatus {
green @0;
yellow @1;
red @2;
danger @3;
ok @0;
warmDEPRECATED @1;
overheated @2;
critical @3;
}
enum NetworkType {
@@ -2076,7 +2079,7 @@ struct DriverStateV2 {
}
}
struct DriverMonitoringState @0xb83cda094a1da284 {
struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 {
events @18 :List(OnroadEvent);
faceDetected @1 :Bool;
isDistracted @2 :Bool;
@@ -2104,6 +2107,75 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
}
}
struct DriverMonitoringState {
lockout @0 :Bool;
alertCountLockoutPercent @1 :Int8;
alertTimeLockoutPercent @2 :Int8;
alwaysOn @3 :Bool;
alwaysOnLockout @4 :Bool;
alertLevel @5 :AlertLevel;
activePolicy @6 :MonitoringPolicy;
isRHD @7 :Bool;
rhdCalibration @8 :CalibrationState;
visionPolicyState @9 :VisionPolicyState;
wheeltouchPolicyState @10 :WheeltouchPolicyState;
enum AlertLevel {
# ordinal must match the name to prevent bugs
# comparing against the raw ordinal value
none @0;
one @1;
two @2;
three @3;
}
enum MonitoringPolicy {
wheeltouch @0;
vision @1;
}
struct VisionPolicyState {
awarenessPercent @0 :Int8;
awarenessStep @1 :Float32;
isDistracted @2 :Bool;
distractedTypes @3 :DistractedTypes;
faceDetected @4 :Bool;
pose @5 :Pose;
wheeltouchFallbackPercent @6 :Int8;
uncertainOffroadAlertPercent @7 :Int8;
struct DistractedTypes {
pose @0: Bool;
eye @1: Bool;
phone @2: Bool;
}
struct Pose {
pitch @0 :Float32;
yaw @1 :Float32;
pitchCalib @2 :CalibrationState;
yawCalib @3 :CalibrationState;
calibrated @4 :Bool;
uncertainty @5 :Float32;
}
}
struct WheeltouchPolicyState {
awarenessPercent @0 :Int8;
awarenessStep @1 :Float32;
driverInteracting @2 :Bool;
}
struct CalibrationState {
calibratedPercent @0 :Int8;
offset @1 :Float32;
}
}
struct Boot {
wallTimeNanos @0 :UInt64;
pstore @4 :Map(Text, Data);
@@ -2377,7 +2449,6 @@ struct Event {
boot @60 :Boot;
# ********** openpilot daemon msgs **********
gpsNMEA @3 :GPSNMEAData;
can @5 :List(CanData);
controlsState @7 :ControlsState;
selfdriveState @130 :SelfdriveState;
@@ -2402,7 +2473,6 @@ struct Event {
qcomGnss @31 :QcomGnss;
gpsLocationExternal @48 :GpsLocationData;
gpsLocation @21 :GpsLocationData;
gnssMeasurements @91 :GnssMeasurements;
liveParameters @61 :LiveParametersData;
liveTorqueParameters @94 :LiveTorqueParametersData;
liveDelay @146 : LiveDelayData;
@@ -2410,7 +2480,7 @@ struct Event {
thumbnail @66: Thumbnail;
onroadEvents @134: List(OnroadEvent);
carParams @69: Car.CarParams;
driverMonitoringState @71: DriverMonitoringState;
driverMonitoringState @151 :DriverMonitoringState;
livePose @129 :LivePose;
modelV2 @75 :ModelDataV2;
drivingModelData @128 :DrivingModelData;
@@ -2436,7 +2506,6 @@ struct Event {
# systems stuff
androidLog @20 :AndroidLogEntry;
managerState @78 :ManagerState;
uploaderState @79 :UploaderState;
procLog @33 :ProcLog;
clocks @35 :Clocks;
deviceState @6 :DeviceState;
@@ -2446,12 +2515,6 @@ struct Event {
# touch frame
touch @135 :List(Touch);
# navigation
navInstruction @82 :NavInstruction;
navRoute @83 :NavRoute;
navThumbnail @84: Thumbnail;
mapRenderState @105: MapRenderState;
# UI services
uiDebug @102 :UIDebug;
@@ -2553,5 +2616,13 @@ struct Event {
gyroscope2DEPRECATED @100 :SensorEventData;
accelerometer2DEPRECATED @101 :SensorEventData;
temperatureSensor2DEPRECATED @123 :SensorEventData;
driverMonitoringStateDEPRECATED @71 :DriverMonitoringStateDEPRECATED;
gpsNMEADEPRECATED @3 :GPSNMEAData;
uploaderStateDEPRECATED @79 :UploaderState;
navInstructionDEPRECATED @82 :NavInstruction;
navRouteDEPRECATED @83 :NavRoute;
navThumbnailDEPRECATED @84 :Thumbnail;
gnssMeasurementsDEPRECATED @91 :GnssMeasurements;
mapRenderStateDEPRECATED @105: MapRenderState;
}
}

View File

@@ -13,7 +13,6 @@ from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any
@@ -105,15 +104,8 @@ def collect_schema(root: Any) -> dict[str, dict]:
return structs
def load_log(cereal_dir: str) -> Any:
import capnp
cereal_dir = os.path.abspath(cereal_dir)
capnp.remove_import_hook()
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir])
def dump_schema(cereal_dir: str, path: str) -> None:
log = load_log(cereal_dir)
def dump_schema(path: str) -> None:
from cereal import log
payload = {
"root": hex_id(log.Event.schema.node.id),
"structs": collect_schema(log.Event.schema),
@@ -214,8 +206,8 @@ def load_peer(path: str) -> dict:
return json.load(handle)
def run_read(cereal_dir: str, peer_path: str) -> int:
log = load_log(cereal_dir)
def run_read(peer_path: str) -> int:
from cereal import log
peer_dump = load_peer(peer_path)
local_dump = {
"root": hex_id(log.Event.schema.node.id),
@@ -243,13 +235,16 @@ def main() -> int:
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp")
args = parser.parse_args()
if args.generate:
dump_schema(args.cereal_dir, args.file)
return 0
return run_read(args.cereal_dir, args.file)
try:
if args.generate:
dump_schema(args.file)
return 0
return run_read(args.file)
except ImportError as exc:
print(f"error: cannot import cereal ({exc}). did scons build cereal?")
return 2
if __name__ == "__main__":

View File

@@ -24,10 +24,7 @@ _services: dict[str, tuple] = {
# note: the "EncodeIdx" packets will still be in the log
"gyroscope": (True, 104., 104),
"accelerometer": (True, 104., 104),
"magnetometer": (True, 25.),
"lightSensor": (True, 100., 100),
"temperatureSensor": (True, 2., 200),
"gpsNMEA": (True, 9.),
"deviceState": (True, 2., 1),
"touch": (True, 20., 1),
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
@@ -56,7 +53,6 @@ _services: dict[str, tuple] = {
"gpsLocation": (True, 1., 1),
"ubloxGnss": (True, 10.),
"qcomGnss": (True, 2.),
"gnssMeasurements": (True, 10., 10),
"clocks": (True, 0.1, 1),
"ubloxRaw": (True, 20.),
"livePose": (True, 20., 4),
@@ -75,10 +71,6 @@ _services: dict[str, tuple] = {
"drivingModelData": (True, 20., 10),
"modelV2": (True, 20., None, QueueSize.BIG),
"managerState": (True, 2., 1),
"uploaderState": (True, 0., 1),
"navInstruction": (True, 1., 10),
"navRoute": (True, 0.),
"navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.),
"userBookmark": (True, 0., 1),
"soundPressure": (True, 10., 10),
@@ -114,8 +106,6 @@ _services: dict[str, tuple] = {
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
"customReservedRawData0": (True, 0.),
"customReservedRawData1": (True, 0.),
"customReservedRawData2": (True, 0.),
}
SERVICE_LIST = {name: Service(*vals) for
idx, (name, vals) in enumerate(_services.items())}

1
common/model.h Normal file
View File

@@ -0,0 +1 @@
#define DEFAULT_MODEL "CD210 (Default)"

View File

@@ -178,7 +178,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -205,7 +204,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// sunnylink params
{"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}},
{"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"ParamsVersion", {PERSISTENT, INT}},
{"SunnylinkCache_Roles", {PERSISTENT, STRING}},
{"SunnylinkCache_Users", {PERSISTENT, STRING}},
{"SunnylinkDongleId", {PERSISTENT, STRING}},

View File

@@ -8,7 +8,7 @@ from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor
from markdown.treeprocessors import Treeprocessor
from zensical.extensions.links import LinksProcessor
from zensical.extensions.links import LinksTreeprocessor
GlossaryTerm = tuple[str, re.Pattern[str], str]
@@ -78,7 +78,7 @@ class GlossaryTreeprocessor(Treeprocessor):
def run(self, root: ET.Element) -> None:
at = self.md.treeprocessors.get_index_for_name("zrelpath")
processor = self.md.treeprocessors[at]
if not isinstance(processor, LinksProcessor):
if not isinstance(processor, LinksTreeprocessor):
raise TypeError("Links processor not registered")
if processor.path == GLOSSARY_PAGE:
return

View File

@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="17.2"
export AGNOS_VERSION="18.1"
fi
export STAGING_ROOT="/data/safe_staging"

Binary file not shown.

View File

@@ -86,7 +86,7 @@ class Car:
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
is_release = False # self.params.get_bool("IsReleaseBranch")
is_release = self.params.get_bool("IsReleaseBranch")
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
if CI is None:

View File

@@ -94,7 +94,7 @@ class TestVCruiseHelper:
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
# Expected diff on enabling. Speed should not change on falling edge of pressed
assert not pressed == self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last
assert (not pressed) == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_resume_in_standstill(self):
"""

View File

@@ -212,7 +212,7 @@ class Controls(ControlsExt):
cs.upAccelCmd = float(self.LoC.pid.p)
cs.uiAccelCmd = float(self.LoC.pid.i)
cs.ufAccelCmd = float(self.LoC.pid.f)
cs.forceDecel = bool((self.sm['driverMonitoringState'].awarenessStatus < 0.) or
cs.forceDecel = bool((self.sm['driverMonitoringState'].alertLevel == log.DriverMonitoringState.AlertLevel.three) or
(self.sm['selfdriveState'].state == State.softDisabling))
lat_tuning = self.CP.lateralTuning.which()

View File

@@ -56,7 +56,7 @@ class DesireHelper:
def get_lane_change_direction(CS):
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
def update(self, carstate, lateral_active, lane_change_prob):
self.alc.update_params()
self.lane_turn_controller.update_params()
v_ego = carstate.vEgo
@@ -88,8 +88,8 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)

View File

@@ -1,10 +1,23 @@
import glob
import json
import os
from itertools import product
from SCons.Script import Value
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.helpers import CompileConfig
from tinygrad import Device
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
MODELD_CONFIGS = [CompileConfig(cam_w, cam_h, prepare_only, 'driving_')
for (cam_w, cam_h), prepare_only in product(CAMERA_CONFIGS, [True, False])]
DM_WARP_CONFIGS = [CompileConfig(cam_w, cam_h, True, 'dm_') for cam_w, cam_h in CAMERA_CONFIGS]
Import('env', 'arch')
chunker_file = File("#common/file_chunker.py")
lenv = env.Clone()
@@ -16,18 +29,17 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
def estimate_pickle_max_size(onnx_size):
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
# get fastest TG config
available = set(Device.get_available_devices())
# FIXME-SP: reset when we bump tg
if False: # 'CUDA' in available:
if 'CUDA' in available:
tg_backend = 'CUDA'
tg_flags = f'DEV={tg_backend}'
elif 'QCOM' in available:
tg_backend = 'QCOM'
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0'
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
else:
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU CPU_LLVM=1' # FIXME-SP: reset when we bump tg
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
tg_flags = f'DEV={tg_backend} THREADS=0'
def write_tg_compiled_flags(target, source, env):
@@ -54,14 +66,35 @@ for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
image_flag = {
'larch64': 'IMAGE=2',
}.get(arch, 'IMAGE=0')
script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
compile_warp_cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py '
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
warp_targets = []
for cam in [_ar_ox_fisheye, _os_fisheye]:
w, h = cam.width, cam.height
warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath]
lenv.Command(warp_targets, tinygrad_files + script_files + [compiled_flags_node], compile_warp_cmd)
modeld_dir = Dir("#selfdrive/modeld").abspath
compile_modeld_script = [File(f"{modeld_dir}/compile_modeld.py")]
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
driving_onnx_deps = [File(f"models/{m}.onnx").abspath for m in ['driving_vision', 'driving_policy']]
driving_metadata_deps = [File(f"models/{m}_metadata.pkl").abspath for m in ['driving_vision', 'driving_policy']]
model_w, model_h = MEDMODEL_INPUT_SIZE
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
for cfg in MODELD_CONFIGS:
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_modeld.py '
f'--model-size {model_w}x{model_h} '
f'--nv12 {",".join(str(x) for x in cfg.nv12)} '
f'--vision-onnx {File("models/driving_vision.onnx").abspath} '
f'--policy-onnx {File("models/driving_policy.onnx").abspath} '
f'--output {cfg.pkl_path} --frame-skip {frame_skip}'
+ (' --prepare-only' if cfg.prepare_only else ''))
node = lenv.Command(cfg.pkl_path, tinygrad_files + compile_modeld_script + driving_onnx_deps + driving_metadata_deps + [chunker_file, compiled_flags_node], cmd)
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
chunk_targets = get_chunk_paths(cfg.pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
def do_chunk(target, source, env, pkl=cfg.pkl_path, chunks=chunk_targets):
chunk_file(pkl, chunks)
lenv.Command(chunk_targets, node, do_chunk)
dm_w, dm_h = DM_INPUT_SIZE
for cfg in DM_WARP_CONFIGS:
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_dm_warp.py '
f'--nv12 {",".join(str(x) for x in cfg.nv12)} --warp-to {dm_w}x{dm_h} '
f'--output {cfg.pkl_path}')
lenv.Command(cfg.pkl_path, tinygrad_files + compile_dm_warp_script + compile_modeld_script + [compiled_flags_node], cmd)
def tg_compile(flags, model_name):
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
@@ -82,7 +115,4 @@ def tg_compile(flags, model_name):
do_chunk,
)
# Compile small models
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
tg_compile(tg_flags, model_name)
tg_compile(tg_flags, 'dmonitoring_model')

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import argparse
import pickle
import time
from tinygrad.tensor import Tensor
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, warp_perspective_tinygrad, _parse_size, _parse_nv12
def make_warp_dm(nv12: NV12Frame, dm_w, dm_h):
cam_w, cam_h, stride, _, _, _ = nv12
stride_pad = stride - cam_w
def warp_dm(input_frame, M_inv):
M_inv = M_inv.to(Device.DEFAULT).realize()
return warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv,
(dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
return warp_dm
def compile_dm_warp(nv12: NV12Frame, dm_w, dm_h, pkl_path):
print(f"Compiling DM warp for {nv12.width}x{nv12.height} -> {dm_w}x{dm_h}...")
warp_dm_jit = TinyJit(make_warp_dm(nv12, dm_w, dm_h), prune=True)
for i in range(10):
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
M_inv = Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')
Device.default.synchronize()
st = time.perf_counter()
warp_dm_jit(frame, M_inv).realize()
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
with open(pkl_path, "wb") as f:
pickle.dump(warp_dm_jit, f)
print(f" Saved to {pkl_path}")
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('--nv12', type=_parse_nv12, required=True,
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
p.add_argument('--warp-to', type=_parse_size, required=True, help='DM input WxH')
p.add_argument('--output', required=True)
args = p.parse_args()
dm_w, dm_h = args.warp_to
compile_dm_warp(args.nv12, dm_w, dm_h, args.output)

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
import argparse
import pickle
import time
from functools import partial
from collections import namedtuple
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad.helpers import Context
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from tinygrad.nn.onnx import OnnxRunner
# https://github.com/tinygrad/tinygrad/issues/15682
from tinygrad.uop.ops import UOp, Ops
_orig = UOp.__reduce__
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
w_dst, h_dst = dst_shape
h_src, w_src = src_shape
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
src_x = src_x / src_w
src_y = src_y / src_w
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
return src_flat[idx]
def frames_to_tensor(frames):
H = (frames.shape[0] * 2) // 3
W = frames.shape[1]
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
frames[1:H:2, 0::2],
frames[0:H:2, 1::2],
frames[1:H:2, 1::2],
frames[H:H+H//4].reshape((H//2, W//2)),
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
return in_img1
def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
uv_offset = stride * y_height
stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
M_inv, (model_w, model_h),
(cam_h, cam_w), stride_pad).realize()
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
tensor = frames_to_tensor(yuv)
return tensor
return frame_prepare_tinygrad
def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip):
img = vision_input_shapes['img'] # (1, 12, 128, 256)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
fb = policy_input_shapes['features_buffer'] # (1, 25, 512)
dp = policy_input_shapes['desire_pulse'] # (1, 25, 8)
tc = policy_input_shapes['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),
}
input_queues = {
'img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
'big_img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
'feat_q': Tensor.zeros(frame_skip * (fb[1] - 1) + 1, fb[0], fb[2]).contiguous().realize(),
'desire_q': Tensor.zeros(frame_skip * dp[1], dp[0], dp[2]).contiguous().realize(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def shift_and_sample(buf, new_val, sample_fn):
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
return sample_fn(buf)
def sample_skip(buf, frame_skip):
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_run_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h,
vision_features_slice, frame_skip, 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_policy(img_q, big_img_q, feat_q, desire_q, desire, traffic_convention, tfm, big_tfm, frame, big_frame):
tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(Device.DEFAULT)
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)
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
if prepare_only:
return img, big_img
vision_out = next(iter(vision_runner({'img': img, 'big_img': 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_pulse': desire_buf, 'traffic_convention': traffic_convention}
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
return vision_out, policy_out
return run_policy
def compile_modeld(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
vision_onnx, policy_onnx, pkl_path):
from get_model_metadata import metadata_path_for
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
vision_runner = OnnxRunner(vision_onnx)
policy_runner = OnnxRunner(policy_onnx)
with open(metadata_path_for(vision_onnx), 'rb') as f:
vision_metadata = pickle.load(f)
vision_features_slice = vision_metadata['output_slices']['hidden_state']
vision_input_shapes = vision_metadata['input_shapes']
with open(metadata_path_for(policy_onnx), 'rb') as f:
policy_input_shapes = pickle.load(f)['input_shapes']
_run = make_run_policy(vision_runner, policy_runner, nv12, model_w, model_h,
vision_features_slice, frame_skip, prepare_only)
run_policy_jit = TinyJit(_run, prune=True)
N_RUNS = 3
SEED = 42
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip)
np.random.seed(seed)
Tensor.manual_seed(seed)
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[:] = np.random.randn(*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()
for o in outs:
# .realize() not needed once jitted, but needed for unjitted fn
o.realize()
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")
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('run unjitted')
_, test_val, test_buffers = random_inputs_run_fn(_run, seed=SEED)
print('capture + replay')
run_policy_jit, _, _ = random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers)
print('pickle round trip')
with open(pkl_path, "wb") as f:
pickle.dump(run_policy_jit, f)
print(f" Saved to {pkl_path}")
with open(pkl_path, "rb") as f:
run_policy_jit = pickle.load(f)
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)
def _parse_size(s):
w, h = s.lower().split('x')
return int(w), int(h)
def _parse_nv12(s):
parts = s.split(',')
assert len(parts) == len(NV12Frame._fields), \
f"--nv12 expects {','.join(NV12Frame._fields)} (got {s!r})"
return NV12Frame(*(int(x) for x in parts))
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
p.add_argument('--nv12', type=_parse_nv12, required=True,
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
p.add_argument('--vision-onnx', required=True)
p.add_argument('--policy-onnx', required=True)
p.add_argument('--output', required=True)
p.add_argument('--prepare-only', action='store_true')
p.add_argument('--frame-skip', type=int, required=True)
args = p.parse_args()
model_w, model_h = args.model_size
compile_modeld(args.nv12, model_w, model_h, args.prepare_only, args.frame_skip,
args.vision_onnx, args.policy_onnx, args.output)

View File

@@ -1,201 +0,0 @@
#!/usr/bin/env python3
import time
import pickle
import numpy as np
from pathlib import Path
from tinygrad.tensor import Tensor
from tinygrad.helpers import Context
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
MODELS_DIR = Path(__file__).parent / 'models'
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
IMG_BUFFER_SHAPE = (30, MEDMODEL_INPUT_SIZE[1] // 2, MEDMODEL_INPUT_SIZE[0] // 2)
def warp_pkl_path(w, h):
return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
def dm_warp_pkl_path(w, h):
return MODELS_DIR / f'dm_warp_{w}x{h}_tinygrad.pkl'
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
w_dst, h_dst = dst_shape
h_src, w_src = src_shape
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
src_x = src_x / src_w
src_y = src_y / src_w
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
return src_flat[idx]
def frames_to_tensor(frames, model_w, model_h):
H = (frames.shape[0] * 2) // 3
W = frames.shape[1]
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
frames[1:H:2, 0::2],
frames[0:H:2, 1::2],
frames[1:H:2, 1::2],
frames[H:H+H//4].reshape((H//2, W//2)),
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
return in_img1
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
uv_offset = stride * y_height
stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
M_inv, (model_w, model_h),
(cam_h, cam_w), stride_pad).realize()
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
tensor = frames_to_tensor(yuv, model_w, model_h)
return tensor
return frame_prepare_tinygrad
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
def make_warp_dm(cam_w, cam_h, dm_w, dm_h):
stride, y_height, _, _ = get_nv12_info(cam_w, cam_h)
stride_pad = stride - cam_w
def warp_dm(input_frame, M_inv):
M_inv = M_inv.to(Device.DEFAULT)
result = warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv, (dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
return result
return warp_dm
def compile_modeld_warp(cam_w, cam_h):
model_w, model_h = MEDMODEL_INPUT_SIZE
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
print(f"Compiling modeld warp for {cam_w}x{cam_h}...")
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.randint(0, 256, yuv_size, dtype=np.uint8)
new_big_frame_np = np.random.randint(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 = warp_pkl_path(cam_w, cam_h)
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"))
jit(*inputs)
def compile_dm_warp(cam_w, cam_h):
dm_w, dm_h = DM_INPUT_SIZE
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
print(f"Compiling DM warp for {cam_w}x{cam_h}...")
warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h)
warp_dm_jit = TinyJit(warp_dm, prune=True)
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
for i in range(10):
inputs = [Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
Device.default.synchronize()
st = time.perf_counter()
warp_dm_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 = dm_warp_pkl_path(cam_w, cam_h)
with open(pkl_path, "wb") as f:
pickle.dump(warp_dm_jit, f)
print(f" Saved to {pkl_path}")
def run_and_save_pickle():
for cam_w, cam_h in CAMERA_CONFIGS:
compile_modeld_warp(cam_w, cam_h)
compile_dm_warp(cam_w, cam_h)
if __name__ == "__main__":
run_and_save_pickle()

View File

@@ -1,12 +1,8 @@
#!/usr/bin/env python3
import os
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
set_tinygrad_backend_from_compiled_flags()
# FIXME-SP: remove once we bump tg
from openpilot.system.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
from tinygrad.tensor import Tensor
import time
import pickle
@@ -32,7 +28,7 @@ class ModelState:
inputs: dict[str, np.ndarray]
output: np.ndarray
def __init__(self):
def __init__(self, cam_w: int, cam_h: int):
with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes']
@@ -44,22 +40,18 @@ class ModelState:
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
self.frame_buf_params = None
self.frame_buf_params = get_nv12_info(cam_w, cam_h)
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
self._blob_cache : dict[int, Tensor] = {}
self.image_warp = None
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
with open(CompileConfig(cam_w, cam_h, prefix='dm_', prepare_only=True).pkl_path, "rb") as f:
self.image_warp = pickle.load(f)
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
self.numpy_inputs['calib'][0,:] = calib
t1 = time.perf_counter()
if self.image_warp is None:
self.frame_buf_params = get_nv12_info(buf.width, buf.height)
warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl'
with open(warp_path, "rb") as f:
self.image_warp = pickle.load(f)
ptr = buf.data.ctypes.data
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
if ptr not in self._blob_cache:
@@ -113,9 +105,6 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t
def main():
config_realtime_process(7, 5)
model = ModelState()
cloudlog.warning("models loaded, dmonitoringmodeld starting")
cloudlog.warning("connecting to driver stream")
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
while not vipc_client.connect(False):
@@ -123,6 +112,9 @@ def main():
assert vipc_client.is_connected()
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
model = ModelState(vipc_client.width, vipc_client.height)
cloudlog.warning("models loaded, dmonitoringmodeld starting")
sm = SubMaster(["liveCalibration"])
pm = PubMaster(["driverStateV2"])

View File

@@ -7,6 +7,10 @@ from typing import Any
from tinygrad.nn.onnx import OnnxPBParser
def metadata_path_for(onnx_path) -> pathlib.Path:
p = pathlib.Path(onnx_path)
return p.parent / (p.stem + '_metadata.pkl')
class MetadataOnnxPBParser(OnnxPBParser):
def _parse_ModelProto(self) -> dict:
@@ -48,7 +52,7 @@ if __name__ == "__main__":
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
}
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')
metadata_path = metadata_path_for(model_path)
with open(metadata_path, 'wb') as f:
pickle.dump(metadata, f)

View File

@@ -0,0 +1,31 @@
import json
import os
from dataclasses import dataclass
from pathlib import Path
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
MODELS_DIR = Path(__file__).resolve().parent / 'models'
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
def set_tinygrad_backend_from_compiled_flags() -> None:
if os.path.isfile(COMPILED_FLAGS_PATH):
with open(COMPILED_FLAGS_PATH) as f:
os.environ['DEV'] = str(json.load(f)['DEV'])
@dataclass
class CompileConfig:
cam_w: int
cam_h: int
prepare_only: bool
prefix: str
@property
def pkl_path(self):
return str(MODELS_DIR / f'{self.prefix}{"warp_" if self.prepare_only else ""}{self.cam_w}x{self.cam_h}_tinygrad.pkl')
@property
def nv12(self):
return (self.cam_w, self.cam_h, *get_nv12_info(self.cam_w, self.cam_h))

View File

@@ -1,12 +1,8 @@
#!/usr/bin/env python3
import os
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
set_tinygrad_backend_from_compiled_flags()
# FIXME-SP: remove once we bump tg
from openpilot.system.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
@@ -30,6 +26,7 @@ from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.common.file_chunker import read_file_chunked
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
@@ -41,17 +38,13 @@ from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
PROCESS_NAME = "selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl'
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3
MIN_LAT_CONTROL_SPEED = 0.3
IMG_QUEUE_SHAPE = (6*(ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ + 1), 128, 256)
assert IMG_QUEUE_SHAPE[0] == 30
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
@@ -86,108 +79,39 @@ class FrameMeta:
if vipc is not None:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
class InputQueues:
def __init__ (self, model_fps, env_fps, n_frames_input):
assert env_fps % model_fps == 0
assert env_fps >= model_fps
self.model_fps = model_fps
self.env_fps = env_fps
self.n_frames_input = n_frames_input
self.dtypes = {}
self.shapes = {}
self.q = {}
def update_dtypes_and_shapes(self, input_dtypes, input_shapes) -> None:
self.dtypes.update(input_dtypes)
if self.env_fps == self.model_fps:
self.shapes.update(input_shapes)
else:
for k in input_shapes:
shape = list(input_shapes[k])
if 'img' in k:
n_channels = shape[1] // self.n_frames_input
shape[1] = (self.env_fps // self.model_fps + (self.n_frames_input - 1)) * n_channels
else:
shape[1] = (self.env_fps // self.model_fps) * shape[1]
self.shapes[k] = tuple(shape)
def reset(self) -> None:
self.q = {k: np.zeros(self.shapes[k], dtype=self.dtypes[k]) for k in self.dtypes.keys()}
def enqueue(self, inputs:dict[str, np.ndarray]) -> None:
for k in inputs.keys():
if inputs[k].dtype != self.dtypes[k]:
raise ValueError(f'supplied input <{k}({inputs[k].dtype})> has wrong dtype, expected {self.dtypes[k]}')
input_shape = list(self.shapes[k])
input_shape[1] = -1
single_input = inputs[k].reshape(tuple(input_shape))
sz = single_input.shape[1]
self.q[k][:,:-sz] = self.q[k][:,sz:]
self.q[k][:,-sz:] = single_input
def get(self, *names) -> dict[str, np.ndarray]:
if self.env_fps == self.model_fps:
return {k: self.q[k] for k in names}
else:
out = {}
for k in names:
shape = self.shapes[k]
if 'img' in k:
n_channels = shape[1] // (self.env_fps // self.model_fps + (self.n_frames_input - 1))
out[k] = np.concatenate([self.q[k][:, s:s+n_channels] for s in np.linspace(0, shape[1] - n_channels, self.n_frames_input, dtype=int)], axis=1)
elif 'pulse' in k:
# any pulse within interval counts
out[k] = self.q[k].reshape((shape[0], shape[1] * self.model_fps // self.env_fps, self.env_fps // self.model_fps, -1)).max(axis=2)
else:
idxs = np.arange(-1, -shape[1], -self.env_fps // self.model_fps)[::-1]
out[k] = self.q[k][:, idxs]
return out
class ModelState(ModelStateBase):
inputs: dict[str, np.ndarray]
output: np.ndarray
prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self):
def __init__(self, cam_w: int, cam_h: int):
ModelStateBase.__init__(self)
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
with open(VISION_METADATA_PATH, 'rb') as f:
vision_metadata = pickle.load(f)
self.vision_input_shapes = vision_metadata['input_shapes']
self.vision_input_names = list(self.vision_input_shapes.keys())
self.vision_output_slices = vision_metadata['output_slices']
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
with open(POLICY_METADATA_PATH, 'rb') as f:
policy_metadata = pickle.load(f)
self.policy_input_shapes = policy_metadata['input_shapes']
self.policy_output_slices = policy_metadata['output_slices']
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
# policy inputs
self.numpy_inputs = {k: np.zeros(self.policy_input_shapes[k], dtype=np.float32) for k in self.policy_input_shapes}
self.full_input_queues = InputQueues(ModelConstants.MODEL_CONTEXT_FREQ, ModelConstants.MODEL_RUN_FREQ, ModelConstants.N_FRAMES)
for k in ['desire_pulse', 'features_buffer']:
self.full_input_queues.update_dtypes_and_shapes({k: self.numpy_inputs[k].dtype}, {k: self.numpy_inputs[k].shape})
self.full_input_queues.reset()
self.img_queues = {'img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize(),
'big_img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize()}
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip)
self.full_frames : dict[str, Tensor] = {}
self._blob_cache : dict[int, Tensor] = {}
self.transforms_np = {k: np.zeros((3,3), dtype=np.float32) for k in self.img_queues}
self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()}
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
self.parser = Parser()
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
self.update_imgs = None
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
self.run_policy = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=False).pkl_path))
self.warp_enqueue = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=True).pkl_path))
self.warp_enqueue(
**self.input_queues,
frame=Tensor.zeros(self.frame_buf_params['img'][3], dtype='uint8').contiguous().realize(),
big_frame=Tensor.zeros(self.frame_buf_params['big_img'][3], dtype='uint8').contiguous().realize())
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
@@ -195,18 +119,6 @@ class ModelState(ModelStateBase):
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:
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
self.prev_desire[:] = inputs['desire_pulse']
if self.update_imgs is None:
for key in bufs.keys():
w, h = bufs[key].width, bufs[key].height
self.frame_buf_params[key] = get_nv12_info(w, h)
warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
with open(warp_path, "rb") as f:
self.update_imgs = pickle.load(f)
for key in bufs.keys():
ptr = bufs[key].data.ctypes.data
yuv_size = self.frame_buf_params[key][3]
@@ -215,30 +127,31 @@ class ModelState(ModelStateBase):
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
self.full_frames[key] = self._blob_cache[cache_key]
for key in bufs.keys():
self.transforms_np[key][:,:] = transforms[key][:,:]
out = self.update_imgs(self.img_queues['img'], self.full_frames['img'], self.transforms['img'],
self.img_queues['big_img'], self.full_frames['big_img'], self.transforms['big_img'])
vision_inputs = {'img': out[0], 'big_img': out[1]}
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
self.npy['desire'][:] = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
self.prev_desire[:] = inputs['desire_pulse']
self.npy['traffic_convention'][:] = inputs['traffic_convention']
self.npy['tfm'][:,:] = transforms['img'][:,:]
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
if prepare_only:
self.warp_enqueue(**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
return None
self.vision_output = self.vision_run(**vision_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices))
vision_output, policy_output = self.run_policy(
**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']
)
self.full_input_queues.enqueue({'features_buffer': vision_outputs_dict['hidden_state'], 'desire_pulse': new_desire})
for k in ['desire_pulse', 'features_buffer']:
self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k]
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
vision_output = vision_output.numpy().flatten()
policy_output = policy_output.numpy().flatten()
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices))
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(policy_output, self.policy_output_slices))
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED:
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
if SEND_RAW_PRED:
combined_outputs_dict['raw_pred'] = np.concatenate([vision_output.copy(), policy_output.copy()])
return combined_outputs_dict
@@ -250,11 +163,6 @@ def main(demo=False):
# also need to move the aux USB interrupts for good timings
config_realtime_process(7, 54)
st = time.monotonic()
cloudlog.warning("loading model")
model = ModelState()
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# visionipc clients
while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False)
@@ -278,6 +186,11 @@ def main(demo=False):
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
st = time.monotonic()
cloudlog.warning("loading model")
model = ModelState(vipc_client_main.width, vipc_client_main.height)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])

View File

@@ -1,12 +0,0 @@
import json
import os
from pathlib import Path
MODELS_DIR = Path(__file__).parent / 'models'
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
def set_tinygrad_backend_from_compiled_flags() -> None:
if os.path.isfile(COMPILED_FLAGS_PATH):
with open(COMPILED_FLAGS_PATH) as f:
os.environ['DEV'] = str(json.load(f)['DEV'])

View File

@@ -1,15 +0,0 @@
# driver monitoring (DM)
Uploading driver-facing camera footage is opt-in, but it is encouraged to opt-in to improve the DM model. You can always change your preference using the "Record and Upload Driver Camera" toggle.
## Troubleshooting
Before creating a bug report, go through these troubleshooting steps.
* Ensure the driver-facing camera has a good view of the driver in normal driving positions.
* This can be checked in Settings -> Device -> Preview Driver Camera (when car is off).
* If the camera can't see the driver, the device should be re-mounted.
## Bug report
In order for us to look into DM bug reports, we'll need the driver-facing camera footage. If you don't normally have this enabled, simply enable the toggle for a single drive. Also ensure the "Upload Raw Logs" toggle is enabled before going for a drive.

View File

@@ -2,7 +2,7 @@
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring
from openpilot.selfdrive.monitoring.policy import DriverMonitoring
def dmonitoringd_thread():
@@ -25,7 +25,7 @@ def dmonitoringd_thread():
valid = sm.all_checks()
if demo_mode and sm.valid['driverStateV2']:
DM.run_step(sm, demo=demo_mode)
DM.run_step(sm, demo=True)
elif valid:
DM.run_step(sm, demo=demo_mode)
@@ -40,8 +40,8 @@ def dmonitoringd_thread():
# save rhd virtual toggle every 5 mins
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
DM.wheelpos_offsetter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
DM.wheel_on_right == (DM.wheelpos_offsetter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
def main():

View File

@@ -1,463 +0,0 @@
from math import atan2, radians
import numpy as np
from cereal import car, log
import cereal.messaging as messaging
from openpilot.selfdrive.selfdrived.events import Events
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.common.realtime import DT_DMON
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.stat_live import RunningStatFilter
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.system.hardware import HARDWARE
EventName = log.OnroadEvent.EventName
# ******************************************************************************************
# NOTE: To fork maintainers.
# Disabling or nerfing safety features will get you and your users banned from our servers.
# We recommend that you do not change these numbers from the defaults.
# ******************************************************************************************
class DRIVER_MONITOR_SETTINGS:
def __init__(self, device_type):
self._DT_DMON = DT_DMON
# ref (page15-16): https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2
self._AWARENESS_TIME = 30. # passive wheeltouch total timeout
self._AWARENESS_PRE_TIME_TILL_TERMINAL = 15.
self._AWARENESS_PROMPT_TIME_TILL_TERMINAL = 6.
self._DISTRACTED_TIME = 11. # active monitoring total timeout
self._DISTRACTED_PRE_TIME_TILL_TERMINAL = 8.
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
self._FACE_THRESHOLD = 0.7
self._EYE_THRESHOLD = 0.5
self._BLINK_THRESHOLD = 0.5
self._PHONE_THRESH = 0.5
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD
self._POSE_YAW_THRESHOLD = 0.4020
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
self._POSE_YAW_MIN_STEER_DEG = 30
self._POSE_YAW_STEER_FACTOR = 0.15
self._POSE_YAW_STEER_MAX_OFFSET = 0.3927
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
self._PITCH_NATURAL_THRESHOLD = 0.449
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
self._PITCH_NATURAL_VAR = 3*0.01
self._YAW_NATURAL_VAR = 3*0.05
self._PITCH_MAX_OFFSET = 0.124
self._PITCH_MIN_OFFSET = -0.0881
self._YAW_MAX_OFFSET = 0.289
self._YAW_MIN_OFFSET = -0.0246
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / self._DT_DMON)
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / self._DT_DMON)
self._POSESTD_THRESHOLD = 0.3
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
self._POSE_CALIB_MIN_SPEED = 13 # 30 mph
self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative
self._POSE_OFFSET_MAX_COUNT = int(360 / self._DT_DMON) # stop deweighting new data after 6 min, aka "short term memory"
self._WHEELPOS_CALIB_MIN_SPEED = 11
self._WHEELPOS_THRESHOLD = 0.5
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side
self._WHEELPOS_DATA_AVG = 0.03
self._WHEELPOS_DATA_VAR = 3*5.5e-5
self._WHEELPOS_MAX_COUNT = -1
self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change
self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change
self._MAX_TERMINAL_ALERTS = 3 # not allowed to engage after 3 terminal alerts
self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts
class DistractedType:
NOT_DISTRACTED = 0
DISTRACTED_POSE = 1 << 0
DISTRACTED_BLINK = 1 << 1
DISTRACTED_PHONE = 1 << 2
class DriverPose:
def __init__(self, settings):
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
self.yaw = 0.
self.pitch = 0.
self.roll = 0.
self.yaw_std = 0.
self.pitch_std = 0.
self.roll_std = 0.
self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.calibrated = False
self.low_std = True
self.cfactor_pitch = 1.
self.cfactor_yaw = 1.
self.steer_yaw_offset = 0.
class DriverProb:
def __init__(self, raw_priors, max_trackable):
self.prob = 0.
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
self.prob_calibrated = False
# model output refers to center of undistorted+leveled image
EFL = 598.0 # focal length in K
cam = DEVICE_CAMERAS[("tici", "ar0231")] # corrected image has same size as raw
W, H = (cam.dcam.width, cam.dcam.height) # corrected image has same size as raw
def face_orientation_from_net(angles_desc, pos_desc, rpy_calib):
# the output of these angles are in device frame
# so from driver's perspective, pitch is up and yaw is right
pitch_net, yaw_net, roll_net = angles_desc
face_pixel_position = ((pos_desc[0]+0.5)*W, (pos_desc[1]+0.5)*H)
yaw_focal_angle = atan2(face_pixel_position[0] - W//2, EFL)
pitch_focal_angle = atan2(face_pixel_position[1] - H//2, EFL)
pitch = pitch_net + pitch_focal_angle
yaw = -yaw_net + yaw_focal_angle
# no calib for roll
pitch -= rpy_calib[1]
yaw -= rpy_calib[2]
return roll_net, pitch, yaw
class DriverMonitoring:
def __init__(self, rhd_saved=False, settings=None, always_on=False):
# init policy settings
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
# init driver status
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
self.pose = DriverPose(settings=self.settings)
self.blink_prob = 0.
self.phone_prob = 0.
self.always_on = always_on
self.distracted_types = []
self.driver_distracted = False
self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON)
self.wheel_on_right = False
self.wheel_on_right_last = None
self.wheel_on_right_default = rhd_saved
self.face_detected = False
self.terminal_alert_cnt = 0
self.terminal_time = 0
self.step_change = 0.
self.active_monitoring_mode = True
self.is_model_uncertain = False
self.hi_stds = 0
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.dcam_uncertain_cnt = 0
self.dcam_uncertain_alerted = False # once per drive
self.dcam_reset_cnt = 0
self.params = Params()
self.too_distracted = self.params.get_bool("DriverTooDistracted")
self._reset_awareness()
self._set_timers(active_monitoring=True)
self._reset_events()
def _reset_awareness(self):
self.awareness = 1.
self.awareness_active = 1.
self.awareness_passive = 1.
def _reset_events(self):
self.current_events = Events()
def _set_timers(self, active_monitoring):
if self.active_monitoring_mode and self.awareness <= self.threshold_prompt:
if active_monitoring:
self.step_change = self.settings._DT_DMON / self.settings._DISTRACTED_TIME
else:
self.step_change = 0.
return # no exploit after orange alert
elif self.awareness <= 0.:
return
if active_monitoring:
# when falling back from passive mode to active mode, reset awareness to avoid false alert
if not self.active_monitoring_mode:
self.awareness_passive = self.awareness
self.awareness = self.awareness_active
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.step_change = self.settings._DT_DMON / self.settings._DISTRACTED_TIME
self.active_monitoring_mode = True
else:
if self.active_monitoring_mode:
self.awareness_active = self.awareness
self.awareness = self.awareness_passive
self.threshold_pre = self.settings._AWARENESS_PRE_TIME_TILL_TERMINAL / self.settings._AWARENESS_TIME
self.threshold_prompt = self.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL / self.settings._AWARENESS_TIME
self.step_change = self.settings._DT_DMON / self.settings._AWARENESS_TIME
self.active_monitoring_mode = False
def _set_policy(self, brake_disengage_prob, car_speed):
bp = brake_disengage_prob
k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2)
bp_normal = max(min(bp / k1, 0.5),0)
self.pose.cfactor_pitch = np.interp(bp_normal, [0, 0.5],
[self.settings._POSE_PITCH_THRESHOLD_SLACK,
self.settings._POSE_PITCH_THRESHOLD_STRICT]) / self.settings._POSE_PITCH_THRESHOLD
self.pose.cfactor_yaw = np.interp(bp_normal, [0, 0.5],
[self.settings._POSE_YAW_THRESHOLD_SLACK,
self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD
def _get_distracted_types(self):
distracted_types = []
if not self.pose.calibrated:
pitch_error = self.pose.pitch - self.settings._PITCH_NATURAL_OFFSET
yaw_error = self.pose.yaw - self.settings._YAW_NATURAL_OFFSET
else:
pitch_error = self.pose.pitch - min(max(self.pose.pitch_offseter.filtered_stat.mean(),
self.settings._PITCH_MIN_OFFSET), self.settings._PITCH_MAX_OFFSET)
yaw_error = self.pose.yaw - min(max(self.pose.yaw_offseter.filtered_stat.mean(),
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
if yaw_error * self.pose.steer_yaw_offset > 0: # unidirectional
yaw_error = max(abs(yaw_error) - min(abs(self.pose.steer_yaw_offset), self.settings._POSE_YAW_STEER_MAX_OFFSET), 0.)
else:
yaw_error = abs(yaw_error)
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
distracted_types.append(DistractedType.DISTRACTED_POSE)
if self.blink_prob > self.settings._BLINK_THRESHOLD:
distracted_types.append(DistractedType.DISTRACTED_BLINK)
if self.phone_prob > self.settings._PHONE_THRESH:
distracted_types.append(DistractedType.DISTRACTED_PHONE)
return distracted_types
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False, steering_angle_deg=0.):
rhd_pred = driver_state.wheelOnRightProb
# calibrates only when there's movement and either face detected
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
self.wheelpos.prob_offseter.push_and_update(rhd_pred)
self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT
if self.wheelpos.prob_calibrated or demo_mode:
self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
else:
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
# make sure no switching when engaged
if op_engaged and self.wheel_on_right_last is not None and self.wheel_on_right_last != self.wheel_on_right and not demo_mode:
self.wheel_on_right = self.wheel_on_right_last
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
driver_data.faceOrientationStd, driver_data.facePositionStd)):
return
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy)
steer_d = max(abs(steering_angle_deg) - self.settings._POSE_YAW_MIN_STEER_DEG, 0.)
self.pose.steer_yaw_offset = radians(steer_d) * -np.sign(steering_angle_deg) * self.settings._POSE_YAW_STEER_FACTOR
if self.wheel_on_right:
self.pose.yaw *= -1
self.pose.steer_yaw_offset *= -1
self.wheel_on_right_last = self.wheel_on_right
self.pose.pitch_std = driver_data.faceOrientationStd[0]
self.pose.yaw_std = driver_data.faceOrientationStd[1]
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
self.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
self.phone_prob = driver_data.phoneProb
self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
or DistractedType.DISTRACTED_POSE in self.distracted_types
or DistractedType.DISTRACTED_BLINK in self.distracted_types) \
and driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std
self.driver_distraction_filter.update(self.driver_distracted)
# update offseter
# only update when driver is actively driving the car above a certain speed
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
if not standstill:
self.dcam_uncertain_cnt += 1
self.dcam_reset_cnt = 0
else:
self.dcam_reset_cnt += 1
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
self.dcam_uncertain_cnt = 0
self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME
self._set_timers(self.face_detected and not self.is_model_uncertain)
if self.face_detected and not self.pose.low_std and not self.driver_distracted:
self.hi_stds += 1
elif self.face_detected and self.pose.low_std:
self.hi_stds = 0
def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
self._reset_events()
# Block engaging until ignition cycle after max number or time of distractions
if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \
self.terminal_time >= self.settings._MAX_TERMINAL_DURATION:
if not self.too_distracted:
self.params.put_bool_nonblocking("DriverTooDistracted", True)
self.too_distracted = True
# Always-on distraction lockout is temporary
if self.too_distracted or (self.always_on and self.awareness <= self.threshold_prompt):
self.current_events.add(EventName.tooDistracted)
always_on_valid = self.always_on and not wrong_gear
if (driver_engaged and self.awareness > 0 and not self.active_monitoring_mode) or \
(not always_on_valid and not op_engaged) or \
(always_on_valid and not op_engaged and self.awareness <= 0):
# always reset on disengage with normal mode; disengage resets only on red if always on
self._reset_awareness()
return
awareness_prev = self.awareness
_reaching_pre = self.awareness - self.step_change <= self.threshold_pre
_reaching_terminal = self.awareness - self.step_change <= 0
standstill_orange_exemption = standstill and _reaching_pre
always_on_red_exemption = always_on_valid and not op_engaged and _reaching_terminal
if self.awareness > 0 and \
((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or standstill_orange_exemption):
if driver_engaged:
self._reset_awareness()
return
# only restore awareness when paying attention and alert is not red
self.awareness = min(self.awareness + ((self.settings._RECOVERY_FACTOR_MAX-self.settings._RECOVERY_FACTOR_MIN)*
(1.-self.awareness)+self.settings._RECOVERY_FACTOR_MIN)*self.step_change, 1.)
if self.awareness == 1.:
self.awareness_passive = min(self.awareness_passive + self.step_change, 1.)
# don't display alert banner when awareness is recovering and has cleared orange
if self.awareness > self.threshold_prompt:
return
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
if certainly_distracted or maybe_distracted:
# should always be counting if distracted unless at standstill and reaching green
# also will not be reaching 0 if DM is active when not engaged
if not (standstill_orange_exemption or always_on_red_exemption):
self.awareness = max(self.awareness - self.step_change, -0.1)
alert = None
if self.awareness <= 0.:
# terminal red alert: disengagement required
alert = EventName.driverDistracted3 if self.active_monitoring_mode else EventName.driverUnresponsive3
self.terminal_time += 1
if awareness_prev > 0.:
self.terminal_alert_cnt += 1
elif self.awareness <= self.threshold_prompt:
# prompt orange alert
alert = EventName.driverDistracted2 if self.active_monitoring_mode else EventName.driverUnresponsive2
elif self.awareness <= self.threshold_pre:
# pre green alert
alert = EventName.driverDistracted1 if self.active_monitoring_mode else EventName.driverUnresponsive1
if alert is not None:
self.current_events.add(alert)
if self.dcam_uncertain_cnt > self.settings._DCAM_UNCERTAIN_ALERT_COUNT and not self.dcam_uncertain_alerted:
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
self.dcam_uncertain_alerted = True
def get_state_packet(self, valid=True):
# build driverMonitoringState packet
dat = messaging.new_message('driverMonitoringState', valid=valid)
dat.driverMonitoringState = {
"events": self.current_events.to_msg(),
"faceDetected": self.face_detected,
"isDistracted": self.driver_distracted,
"distractedType": sum(self.distracted_types),
"awarenessStatus": self.awareness,
"posePitchOffset": self.pose.pitch_offseter.filtered_stat.mean(),
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
"stepChange": self.step_change,
"awarenessActive": self.awareness_active,
"awarenessPassive": self.awareness_passive,
"isLowStd": self.pose.low_std,
"hiStdCount": self.hi_stds,
"isActiveMode": self.active_monitoring_mode,
"isRHD": self.wheel_on_right,
"uncertainCount": self.dcam_uncertain_cnt,
}
return dat
def run_step(self, sm, demo=False):
if demo:
highway_speed = 30
enabled = True
wrong_gear = False
standstill = False
driver_engaged = False
brake_disengage_prob = 1.0
rpyCalib = [0., 0., 0.]
else:
highway_speed = sm['carState'].vEgo
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
standstill = sm['carState'].standstill
driver_engaged = sm['carState'].steeringPressed or (sm['selfdriveState'].enabled and sm['carState'].gasPressed)
brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s
rpyCalib = sm['liveCalibration'].rpyCalib
self._set_policy(
brake_disengage_prob=brake_disengage_prob,
car_speed=highway_speed,
)
# Parse data from dmonitoringmodeld
self._update_states(
driver_state=sm['driverStateV2'],
cal_rpy=rpyCalib,
car_speed=highway_speed,
op_engaged=enabled,
standstill=standstill,
demo_mode=demo,
steering_angle_deg=sm['carState'].steeringAngleDeg,
)
# Update distraction events
self._update_events(
driver_engaged=driver_engaged,
op_engaged=enabled,
standstill=standstill,
wrong_gear=wrong_gear,
car_speed=highway_speed
)

View File

@@ -0,0 +1,426 @@
from collections import defaultdict
from math import atan2, radians
import numpy as np
from cereal import car, log
import cereal.messaging as messaging
from openpilot.common.realtime import DT_DMON
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.stat_live import RunningStatFilter
from openpilot.common.transformations.camera import DEVICE_CAMERAS
AlertLevel = log.DriverMonitoringState.AlertLevel
MonitoringPolicy = log.DriverMonitoringState.MonitoringPolicy
def to_percent(v):
return int(min(max(v * 100., 0.), 100.))
# ******************************************************************************************
# NOTE: To fork maintainers.
# Disabling or nerfing safety features will get you and your users banned from our servers.
# We recommend that you do not change these numbers from the defaults.
# ******************************************************************************************
class DRIVER_MONITOR_SETTINGS:
def __init__(self):
# https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2
self._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT = 15.
self._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT = 24.
self._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT = 30.
# https://cdn.euroncap.com/cars/assets/euro_ncap_protocol_safe_driving_driver_engagement_v11_a30e874152.pdf
self._VISION_POLICY_ALERT_1_TIMEOUT = 3.
self._VISION_POLICY_ALERT_2_TIMEOUT = 5.
self._VISION_POLICY_ALERT_3_TIMEOUT = 11.
self._TIMEOUT_RECOVERY_FACTOR_MAX = 5.
self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25
self._MAX_TERMINAL_ALERTS = 3 # not allowed to engage after 3 terminal alerts
self._MAX_TERMINAL_DURATION = int(30 / DT_DMON) # not allowed to engage after 30s of terminal alerts
self._FACE_THRESHOLD = 0.7
self._EYE_THRESHOLD = 0.5
self._BLINK_THRESHOLD = 0.5
self._PHONE_THRESH = 0.5
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD
self._POSE_YAW_THRESHOLD = 0.4020
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
self._POSE_YAW_MIN_STEER_DEG = 30
self._POSE_YAW_STEER_FACTOR = 0.15
self._POSE_YAW_STEER_MAX_OFFSET = 0.3927
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
self._PITCH_NATURAL_THRESHOLD = 0.449
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
self._PITCH_NATURAL_VAR = 3*0.01
self._YAW_NATURAL_VAR = 3*0.05
self._PITCH_MAX_OFFSET = 0.124
self._PITCH_MIN_OFFSET = -0.0881
self._YAW_MAX_OFFSET = 0.289
self._YAW_MIN_OFFSET = -0.0246
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / DT_DMON)
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / DT_DMON)
self._HI_STD_THRESHOLD = 0.3
self._HI_STD_FALLBACK_TIME = int(10 / DT_DMON) # fall back to wheel touch if model is uncertain for 10s
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
self._POSE_CALIB_MIN_SPEED = 13 # 30 mph
self._POSE_OFFSET_MIN_COUNT = int(60 / DT_DMON) # valid data counts before calibration completes, 1min cumulative
self._POSE_OFFSET_MAX_COUNT = int(360 / DT_DMON) # stop deweighting new data after 6 min, aka "short term memory"
self._WHEELPOS_CALIB_MIN_SPEED = 11
self._WHEELPOS_THRESHOLD = 0.5
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / DT_DMON) # allow 15 seconds to converge wheel side
self._WHEELPOS_DATA_AVG = 0.03
self._WHEELPOS_DATA_VAR = 3*5.5e-5
self._WHEELPOS_MAX_COUNT = -1
class DriverPose:
def __init__(self, settings):
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
self.yaw = 0.
self.pitch = 0.
self.pitch_offsetter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.yaw_offsetter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.calibrated = False
self.low_std = True
self.cfactor_pitch = 1.
self.cfactor_yaw = 1.
self.steer_yaw_offset = 0.
# model output refers to center of undistorted+leveled image
ref_undistorted_cam = DEVICE_CAMERAS[("tici", "ar0231")].dcam
dcam_undistorted_FL = 598.0
dcam_undistorted_W, dcam_undistorted_H = (ref_undistorted_cam.width, ref_undistorted_cam.height)
def face_orientation_from_model(orient_model, pos_model, rpy_calib):
pitch_model = orient_model[0]
yaw_model = orient_model[1]
face_pixel_position = ((pos_model[0]+0.5)*dcam_undistorted_W, (pos_model[1]+0.5)*dcam_undistorted_H)
yaw_focal_angle = atan2(face_pixel_position[0] - dcam_undistorted_W//2, dcam_undistorted_FL)
pitch_focal_angle = atan2(face_pixel_position[1] - dcam_undistorted_H//2, dcam_undistorted_FL)
pitch = pitch_model + pitch_focal_angle
yaw = -yaw_model + yaw_focal_angle
pitch -= rpy_calib[1]
yaw -= rpy_calib[2]
return pitch, yaw
class DriverMonitoring:
def __init__(self, rhd_saved=False, settings=None, always_on=False):
# init policy settings
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS()
# init driver status
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
self.wheelpos_offsetter = RunningStatFilter(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
self.pose = DriverPose(settings=self.settings)
self.blink_prob = 0.
self.phone_prob = 0.
self.alert_level = AlertLevel.none
self.always_on = always_on
self.distracted_types = defaultdict(bool)
self.driver_distracted = False
self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, DT_DMON)
self.wheel_on_right = False
self.wheel_on_right_last = None
self.wheel_on_right_default = rhd_saved
self.face_detected = False
self.terminal_alert_cnt = 0
self.terminal_time = 0
self.step_change = 0.
self.active_policy = MonitoringPolicy.vision
self.driver_interacting = False
self.is_model_uncertain = False
self.hi_stds = 0
self.model_std_max = 0.
self.threshold_alert_1 = 0.
self.threshold_alert_2 = 0.
self.dcam_uncertain_cnt = 0
self.dcam_reset_cnt = 0
self.too_distracted = Params().get_bool("DriverTooDistracted")
self._reset_awareness()
self._set_policy(MonitoringPolicy.vision)
def _reset_awareness(self):
self.awareness = 1.
self.last_vision_awareness = 1.
self.last_wheeltouch_awareness = 1.
def _set_policy(self, target_policy):
if self.active_policy == MonitoringPolicy.vision and self.awareness <= self.threshold_alert_2:
if target_policy == MonitoringPolicy.vision:
self.step_change = DT_DMON / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
else:
self.step_change = 0.
return # no exploit after orange alert
elif self.awareness <= 0.:
return
if target_policy == MonitoringPolicy.vision:
# when falling back from passive mode to active mode, reset awareness to avoid false alert
if self.active_policy != MonitoringPolicy.vision:
self.last_wheeltouch_awareness = self.awareness
self.awareness = self.last_vision_awareness
self.threshold_alert_1 = 1. - self.settings._VISION_POLICY_ALERT_1_TIMEOUT / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
self.threshold_alert_2 = 1. - self.settings._VISION_POLICY_ALERT_2_TIMEOUT / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
self.step_change = DT_DMON / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
self.active_policy = MonitoringPolicy.vision
else:
if self.active_policy == MonitoringPolicy.vision:
self.last_vision_awareness = self.awareness
self.awareness = self.last_wheeltouch_awareness
self.threshold_alert_1 = 1. - self.settings._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT / self.settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT
self.threshold_alert_2 = 1. - self.settings._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT / self.settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT
self.step_change = DT_DMON / self.settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT
self.active_policy = MonitoringPolicy.wheeltouch
def _set_pose_strictness(self, brake_disengage_prob, car_speed):
bp = brake_disengage_prob
k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2)
bp_normal = max(min(bp / k1, 0.5),0)
self.pose.cfactor_pitch = np.interp(bp_normal, [0, 0.5],
[self.settings._POSE_PITCH_THRESHOLD_SLACK,
self.settings._POSE_PITCH_THRESHOLD_STRICT]) / self.settings._POSE_PITCH_THRESHOLD
self.pose.cfactor_yaw = np.interp(bp_normal, [0, 0.5],
[self.settings._POSE_YAW_THRESHOLD_SLACK,
self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD
def _get_distracted_types(self):
self.distracted_types = defaultdict(bool)
if not self.pose.calibrated:
pitch_error = self.pose.pitch - self.settings._PITCH_NATURAL_OFFSET
yaw_error = self.pose.yaw - self.settings._YAW_NATURAL_OFFSET
else:
pitch_error = self.pose.pitch - min(max(self.pose.pitch_offsetter.filtered_stat.mean(),
self.settings._PITCH_MIN_OFFSET), self.settings._PITCH_MAX_OFFSET)
yaw_error = self.pose.yaw - min(max(self.pose.yaw_offsetter.filtered_stat.mean(),
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
if yaw_error * self.pose.steer_yaw_offset > 0: # unidirectional
yaw_error = max(abs(yaw_error) - min(abs(self.pose.steer_yaw_offset), self.settings._POSE_YAW_STEER_MAX_OFFSET), 0.)
else:
yaw_error = abs(yaw_error)
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
self.distracted_types['pose'] = bool((pitch_error > pitch_threshold) or (yaw_error > yaw_threshold))
self.distracted_types['eye'] = bool(self.blink_prob > self.settings._BLINK_THRESHOLD)
self.distracted_types['phone'] = bool(self.phone_prob > self.settings._PHONE_THRESH)
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False, steering_angle_deg=0.):
rhd_pred = driver_state.wheelOnRightProb
# calibrates only when there's movement and either face detected
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
self.wheelpos_offsetter.push_and_update(rhd_pred)
wheelpos_calibrated = self.wheelpos_offsetter.filtered_stat.n >= self.settings._WHEELPOS_FILTER_MIN_COUNT
if wheelpos_calibrated or demo_mode:
self.wheel_on_right = self.wheelpos_offsetter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
else:
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
# make sure no switching when engaged
if op_engaged and self.wheel_on_right_last is not None and self.wheel_on_right_last != self.wheel_on_right and not demo_mode:
self.wheel_on_right = self.wheel_on_right_last
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
driver_data.faceOrientationStd, driver_data.facePositionStd)):
return
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
self.pose.pitch, self.pose.yaw = face_orientation_from_model(driver_data.faceOrientation, driver_data.facePosition, cal_rpy)
steer_d = max(abs(steering_angle_deg) - self.settings._POSE_YAW_MIN_STEER_DEG, 0.)
self.pose.steer_yaw_offset = radians(steer_d) * -np.sign(steering_angle_deg) * self.settings._POSE_YAW_STEER_FACTOR
if self.wheel_on_right:
self.pose.yaw *= -1
self.pose.steer_yaw_offset *= -1
self.wheel_on_right_last = self.wheel_on_right
self.model_std_max = max(driver_data.faceOrientationStd[0], driver_data.faceOrientationStd[1])
self.pose.low_std = self.model_std_max < self.settings._HI_STD_THRESHOLD
self.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
self.phone_prob = driver_data.phoneProb
self._get_distracted_types()
self.driver_distracted = any(self.distracted_types.values()) and driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std
self.driver_distraction_filter.update(self.driver_distracted)
# only update offsetter when driver is actively driving the car above a certain speed
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
self.pose.pitch_offsetter.push_and_update(self.pose.pitch)
self.pose.yaw_offsetter.push_and_update(self.pose.yaw)
self.pose.calibrated = self.pose.pitch_offsetter.filtered_stat.n >= self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offsetter.filtered_stat.n >= self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
dcam_uncertain = self.model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD
if dcam_uncertain and not standstill:
self.dcam_uncertain_cnt += 1
self.dcam_reset_cnt = 0
else:
self.dcam_reset_cnt += 1
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
self.dcam_uncertain_cnt = 0
self.is_model_uncertain = self.hi_stds >= self.settings._HI_STD_FALLBACK_TIME
self._set_policy(MonitoringPolicy.vision if self.face_detected and not self.is_model_uncertain else MonitoringPolicy.wheeltouch)
if self.face_detected and not self.pose.low_std and not self.driver_distracted:
self.hi_stds += 1
elif self.face_detected and self.pose.low_std:
self.hi_stds = 0
def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear):
self.alert_level = AlertLevel.none
self.driver_interacting = driver_engaged
if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \
self.terminal_time >= self.settings._MAX_TERMINAL_DURATION:
self.too_distracted = True
always_on_valid = self.always_on and not wrong_gear
if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \
(not always_on_valid and not op_engaged) or \
(always_on_valid and not op_engaged and self.awareness <= 0):
# always reset on disengage with normal mode; disengage resets only on red if always on
self._reset_awareness()
return
awareness_prev = self.awareness
_reaching_alert_1 = self.awareness - self.step_change <= self.threshold_alert_1
_reaching_alert_3 = self.awareness - self.step_change <= 0
standstill_exemption = standstill and _reaching_alert_1
always_on_exemption = always_on_valid and not op_engaged and _reaching_alert_3
if self.awareness > 0 and \
((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or standstill_exemption):
if self.driver_interacting:
self._reset_awareness()
return
# only restore awareness when paying attention and alert is not red
self.awareness = min(self.awareness + ((self.settings._TIMEOUT_RECOVERY_FACTOR_MAX-self.settings._TIMEOUT_RECOVERY_FACTOR_MIN)*
(1.-self.awareness)+self.settings._TIMEOUT_RECOVERY_FACTOR_MIN)*self.step_change, 1.)
if self.awareness == 1.:
self.last_wheeltouch_awareness = min(self.last_wheeltouch_awareness + self.step_change, 1.)
# don't display alert banner when awareness is recovering and has cleared orange
if self.awareness > self.threshold_alert_2:
return
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
maybe_distracted = self.is_model_uncertain or not self.face_detected
if certainly_distracted or maybe_distracted:
# should always be counting if distracted unless at standstill and reaching green
# also will not be reaching 0 if DM is active when not engaged
if not (standstill_exemption or always_on_exemption):
self.awareness = max(self.awareness - self.step_change, -0.1)
if self.awareness <= 0.:
# terminal alert: disengagement required
self.alert_level = AlertLevel.three
self.terminal_time += 1
if awareness_prev > 0.:
self.terminal_alert_cnt += 1
elif self.awareness <= self.threshold_alert_2:
self.alert_level = AlertLevel.two
elif self.awareness <= self.threshold_alert_1:
self.alert_level = AlertLevel.one
def get_state_packet(self, valid=True):
# build driverMonitoringState packet
dat = messaging.new_message('driverMonitoringState', valid=valid)
dm = dat.driverMonitoringState
dm.lockout = self.too_distracted
dm.alertCountLockoutPercent = to_percent(self.terminal_alert_cnt / self.settings._MAX_TERMINAL_ALERTS)
dm.alertTimeLockoutPercent = to_percent(self.terminal_time / self.settings._MAX_TERMINAL_DURATION)
dm.alwaysOn = self.always_on
dm.alwaysOnLockout = self.always_on and self.awareness <= self.threshold_alert_2
dm.alertLevel = self.alert_level
dm.activePolicy = self.active_policy
dm.isRHD = self.wheel_on_right
dm.rhdCalibration.calibratedPercent = to_percent(self.wheelpos_offsetter.filtered_stat.n / self.settings._WHEELPOS_FILTER_MIN_COUNT)
dm.rhdCalibration.offset = self.wheelpos_offsetter.filtered_stat.M
dm.visionPolicyState.awarenessPercent = to_percent(self.last_vision_awareness if self.active_policy != MonitoringPolicy.vision else self.awareness)
dm.visionPolicyState.awarenessStep = self.step_change if self.active_policy == MonitoringPolicy.vision else 0.
dm.visionPolicyState.isDistracted = self.driver_distracted
dm.visionPolicyState.distractedTypes.pose = self.distracted_types['pose']
dm.visionPolicyState.distractedTypes.eye = self.distracted_types['eye']
dm.visionPolicyState.distractedTypes.phone = self.distracted_types['phone']
dm.visionPolicyState.faceDetected = self.face_detected
dm.visionPolicyState.pose.pitch = self.pose.pitch
dm.visionPolicyState.pose.yaw = self.pose.yaw
dm.visionPolicyState.pose.calibrated = self.pose.calibrated
dm.visionPolicyState.pose.pitchCalib.calibratedPercent = to_percent(self.pose.pitch_offsetter.filtered_stat.n / self.settings._POSE_OFFSET_MIN_COUNT)
dm.visionPolicyState.pose.pitchCalib.offset = self.pose.pitch_offsetter.filtered_stat.M
dm.visionPolicyState.pose.yawCalib.calibratedPercent = to_percent(self.pose.yaw_offsetter.filtered_stat.n / self.settings._POSE_OFFSET_MIN_COUNT)
dm.visionPolicyState.pose.yawCalib.offset = self.pose.yaw_offsetter.filtered_stat.M
dm.visionPolicyState.pose.uncertainty = self.model_std_max
dm.visionPolicyState.wheeltouchFallbackPercent = to_percent(self.hi_stds / self.settings._HI_STD_FALLBACK_TIME)
dm.visionPolicyState.uncertainOffroadAlertPercent = to_percent(self.dcam_uncertain_cnt / self.settings._DCAM_UNCERTAIN_ALERT_COUNT)
dm.wheeltouchPolicyState.awarenessPercent = to_percent(self.last_wheeltouch_awareness if self.active_policy == MonitoringPolicy.vision else self.awareness)
dm.wheeltouchPolicyState.awarenessStep = 0. if self.active_policy == MonitoringPolicy.vision else self.step_change
dm.wheeltouchPolicyState.driverInteracting = self.driver_interacting
return dat
def run_step(self, sm, demo=False):
if demo:
car_speed = 30
enabled = True
wrong_gear = False
standstill = False
driver_engaged = False
brake_disengage_prob = 1.0
steering_angle_deg = 0.0
rpyCalib = [0., 0., 0.]
else:
car_speed = sm['carState'].vEgo
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
standstill = sm['carState'].standstill
driver_engaged = sm['carState'].steeringPressed or sm['carState'].gasPressed
brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s
steering_angle_deg = sm['carState'].steeringAngleDeg
rpyCalib = sm['liveCalibration'].rpyCalib
self._set_pose_strictness(
brake_disengage_prob=brake_disengage_prob,
car_speed=car_speed,
)
# Parse data from dmonitoringmodeld
self._update_states(
driver_state=sm['driverStateV2'],
cal_rpy=rpyCalib,
car_speed=car_speed,
op_engaged=enabled,
standstill=standstill,
demo_mode=demo,
steering_angle_deg=steering_angle_deg,
)
# Update distraction events
self._update_events(
driver_engaged=driver_engaged,
op_engaged=enabled,
standstill=standstill,
wrong_gear=wrong_gear,
)

View File

@@ -3,17 +3,16 @@ import pytest
from cereal import log, car
from openpilot.common.realtime import DT_DMON
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
from openpilot.system.hardware import HARDWARE
from openpilot.selfdrive.monitoring.policy import DriverMonitoring, DRIVER_MONITOR_SETTINGS
EventName = log.OnroadEvent.EventName
dm_settings = DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
dm_settings = DRIVER_MONITOR_SETTINGS()
TEST_TIMESPAN = 120 # seconds
DISTRACTED_SECONDS_TO_ORANGE = dm_settings._DISTRACTED_TIME - dm_settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL + 1
DISTRACTED_SECONDS_TO_RED = dm_settings._DISTRACTED_TIME + 1
INVISIBLE_SECONDS_TO_ORANGE = dm_settings._AWARENESS_TIME - dm_settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL + 1
INVISIBLE_SECONDS_TO_RED = dm_settings._AWARENESS_TIME + 1
DISTRACTED_SECONDS_TO_ORANGE = dm_settings._VISION_POLICY_ALERT_2_TIMEOUT + 1
DISTRACTED_SECONDS_TO_RED = dm_settings._VISION_POLICY_ALERT_3_TIMEOUT + 1
INVISIBLE_SECONDS_TO_ORANGE = dm_settings._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT + 1
INVISIBLE_SECONDS_TO_RED = dm_settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT + 1
def make_msg(face_detected, distracted=False, model_uncertain=False):
ds = log.DriverStateV2.new_message()
@@ -35,7 +34,7 @@ msg_ATTENTIVE = make_msg(True)
msg_DISTRACTED = make_msg(True, distracted=True)
msg_ATTENTIVE_UNCERTAIN = make_msg(True, model_uncertain=True)
msg_DISTRACTED_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=True)
msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=dm_settings._POSESTD_THRESHOLD*1.5)
msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=dm_settings._HI_STD_THRESHOLD*1.5)
# driver interaction with car
car_interaction_DETECTED = True
@@ -51,49 +50,49 @@ always_false = [False] * int(TEST_TIMESPAN / DT_DMON)
class TestMonitoring:
def _run_seq(self, msgs, interaction, engaged, standstill):
DM = DriverMonitoring()
events = []
alert_lvls = []
for idx in range(len(msgs)):
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx])
# cal_rpy and car_speed don't matter here
# evaluate events at 10Hz for tests
DM._update_events(interaction[idx], engaged[idx], standstill[idx], 0, 0)
events.append(DM.current_events)
assert len(events) == len(msgs), f"got {len(events)} for {len(msgs)} driverState input msgs"
return events, DM
DM._update_events(interaction[idx], engaged[idx], standstill[idx], 0)
alert_lvls.append(DM.alert_level)
assert len(alert_lvls) == len(msgs), f"got {len(alert_lvls)} for {len(msgs)} driverState input msgs"
return alert_lvls, DM
def _assert_no_events(self, events):
assert all(not len(e) for e in events)
# engaged, driver is attentive all the time
def test_fully_aware_driver(self):
events, _ = self._run_seq(always_attentive, always_false, always_true, always_false)
self._assert_no_events(events)
alert_lvls, d_status = self._run_seq(always_attentive, always_false, always_true, always_false)
assert all(a == 0 for a in alert_lvls)
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.vision
# engaged, driver is distracted and does nothing
def test_fully_distracted_driver(self):
events, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
assert len(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL + \
((d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
EventName.driverDistracted1
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL + \
((d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.driverDistracted2
assert events[int((d_status.settings._DISTRACTED_TIME + \
((TEST_TIMESPAN-10-d_status.settings._DISTRACTED_TIME)/2))/DT_DMON)].names[0] == EventName.driverDistracted3
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
s = d_status.settings
assert alert_lvls[int(s._VISION_POLICY_ALERT_1_TIMEOUT / 2 / DT_DMON)] == 0
assert alert_lvls[int((s._VISION_POLICY_ALERT_1_TIMEOUT + \
(s._VISION_POLICY_ALERT_2_TIMEOUT - s._VISION_POLICY_ALERT_1_TIMEOUT) / 2) / DT_DMON)] == 1
assert alert_lvls[int((s._VISION_POLICY_ALERT_2_TIMEOUT + \
(s._VISION_POLICY_ALERT_3_TIMEOUT - s._VISION_POLICY_ALERT_2_TIMEOUT) / 2) / DT_DMON)] == 2
assert alert_lvls[int((s._VISION_POLICY_ALERT_3_TIMEOUT + \
(TEST_TIMESPAN - 10 - s._VISION_POLICY_ALERT_3_TIMEOUT) / 2) / DT_DMON)] == 3
assert isinstance(d_status.awareness, float)
# engaged, no face detected the whole time, no action
def test_fully_invisible_driver(self):
events, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
assert len(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL + \
((d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
EventName.driverUnresponsive1
assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL + \
((d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert events[int((d_status.settings._AWARENESS_TIME + \
((TEST_TIMESPAN-10-d_status.settings._AWARENESS_TIME)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive3
alert_lvls, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
s = d_status.settings
assert alert_lvls[int(s._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT / 2 / DT_DMON)] == 0
assert alert_lvls[int((s._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT + \
(s._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT - s._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT) / 2) / DT_DMON)] == 1
assert alert_lvls[int((s._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT + \
(s._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT - s._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT) / 2) / DT_DMON)] == 2
assert alert_lvls[int((s._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT + \
(TEST_TIMESPAN - 10 - s._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT) / 2) / DT_DMON)] == 3
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch
# engaged, down to orange, driver pays attention, back to normal; then down to orange, driver touches wheel
# - should have short orange recovery time and no green afterwards; wheel touch only recovers when paying attention
@@ -104,13 +103,13 @@ class TestMonitoring:
[msg_ATTENTIVE] * (int(TEST_TIMESPAN/DT_DMON)-int((DISTRACTED_SECONDS_TO_ORANGE*3+2)/DT_DMON))
interaction_vector = [car_interaction_NOT_DETECTED] * int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON) + \
[car_interaction_DETECTED] * (int(TEST_TIMESPAN/DT_DMON)-int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON))
events, _ = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
assert events[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)]) == 0
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert len(events[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)]) == 0
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
assert alert_lvls[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2
assert alert_lvls[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)] == 0
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)] == 2
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)] == 2
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)] == 0
# engaged, down to orange, driver dodges camera, then comes back still distracted, down to red, \
# driver dodges, and then touches wheel to no avail, disengages and reengages
@@ -128,11 +127,11 @@ class TestMonitoring:
= [True] * int(1/DT_DMON)
op_vector[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+2.5)/DT_DMON):int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3)/DT_DMON)] \
= [False] * int(0.5/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
assert events[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted2
assert events[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted3
assert events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)].names[0] == EventName.driverDistracted3
assert len(events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)]) == 0
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)] == 2
assert alert_lvls[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)] == 3
assert alert_lvls[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)] == 3
assert alert_lvls[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)] == 0
# engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears
# - both actions should clear the alert, but momentary appearance should not
@@ -143,16 +142,16 @@ class TestMonitoring:
ds_vector[int((2*INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON):int((2*INVISIBLE_SECONDS_TO_ORANGE+1+_visible_time)/DT_DMON)] = \
[msg_ATTENTIVE] * int(_visible_time/DT_DMON)
interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false)
assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)]) == 0
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false)
assert alert_lvls[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)] == 0
if _visible_time == 0.5:
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive1
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 1
elif _visible_time == 10:
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)]) == 0
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 0
# engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages
# - only disengage will clear the alert
@@ -164,19 +163,19 @@ class TestMonitoring:
ds_vector[int(INVISIBLE_SECONDS_TO_RED/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON)] = [msg_ATTENTIVE] * int(_visible_time/DT_DMON)
interaction_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON)] = [True] * int(1/DT_DMON)
op_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] = [False] * int(0.5/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert events[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive3
assert events[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive3
assert events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)].names[0] == EventName.driverUnresponsive3
assert len(events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)]) == 0
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
assert alert_lvls[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)] == 3
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)] == 3
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] == 3
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)] == 0
# disengaged, always distracted driver
# - dm should stay quiet when not engaged
def test_pure_dashcam_user(self):
events, _ = self._run_seq(always_distracted, always_false, always_false, always_false)
assert sum(len(event) for event in events) == 0
alert_lvls, _ = self._run_seq(always_distracted, always_false, always_false, always_false)
assert all(a == 0 for a in alert_lvls)
# engaged, car stops at traffic light, down to orange, no action, then car starts moving
# - should only reach green when stopped, but continues counting down on launch
@@ -184,11 +183,12 @@ class TestMonitoring:
_redlight_time = 60 # seconds
standstill_vector = always_true[:]
standstill_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON)
events, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
assert len(events[int((_redlight_time-0.1)/DT_DMON)]) == 0
_pre_to_prompt = d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL - d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL
assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.driverDistracted1
assert events[int((_redlight_time+_pre_to_prompt+0.5)/DT_DMON)].names[0] == EventName.driverDistracted2
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
s = d_status.settings
assert alert_lvls[int((_redlight_time-0.1)/DT_DMON)] == 0
_alert_1_to_2 = s._VISION_POLICY_ALERT_2_TIMEOUT - s._VISION_POLICY_ALERT_1_TIMEOUT
assert alert_lvls[int((_redlight_time+0.5)/DT_DMON)] == 1
assert alert_lvls[int((_redlight_time+_alert_1_to_2+0.5)/DT_DMON)] == 2
# engaged, distracted while moving, then car stops after reaching orange
# - should reset timer to pre green at standstill
@@ -196,67 +196,81 @@ class TestMonitoring:
_stop_time = DISTRACTED_SECONDS_TO_ORANGE + 1 # stop 1 second after reaching orange
standstill_vector = always_false[:]
standstill_vector[int(_stop_time/DT_DMON):] = [True] * int((TEST_TIMESPAN-_stop_time)/DT_DMON)
events, _ = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
alert_lvls, _ = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
# just before and briefly after stopping: orange alert; goes away quickly after stopped
assert events[int((_stop_time+0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert len(events[int((_stop_time+0.5)/DT_DMON)]) == 0
assert alert_lvls[int((_stop_time+0.1)/DT_DMON)] == 2
assert alert_lvls[int((_stop_time+0.5)/DT_DMON)] == 0
# engaged, model is somehow uncertain and driver is distracted
# - should fall back to wheel touch after uncertain alert
def test_somehow_indecisive_model(self):
ds_vector = [msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN] * int(TEST_TIMESPAN/DT_DMON)
interaction_vector = always_false[:]
events, d_status = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
assert EventName.driverUnresponsive1 in \
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)].names
assert EventName.driverUnresponsive2 in \
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
assert EventName.driverUnresponsive3 in \
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
alert_lvls, d_status = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
s = d_status.settings
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*s._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)] == 1
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*s._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)] == 2
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*s._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)] == 3
def _build_sm(selfdrive_enabled, lat_active, steering_pressed, gas_pressed):
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
(False, False, False), # Both Disabled
(True, False, True), # OP Enabled, Lat Inactive
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
(True, True, True) # Both Active
])
def test_enabled_states(enabled_state, lat_active_state, expected):
"""
Test DriverMonitoring.run_step with all 4 combinations of:
- selfdriveState.enabled (True/False)
- carControl.latActive (True/False)
"""
cs = car.CarState.new_message()
cs.vEgo = 30.0
cs.gearShifter = car.CarState.GearShifter.drive
cs.steeringPressed = steering_pressed
cs.gasPressed = gas_pressed
cs.standstill = False
cs.steeringPressed = False
cs.gasPressed = False
ss = log.SelfdriveState.new_message()
ss.enabled = selfdrive_enabled
ss.enabled = enabled_state
cc = car.CarControl.new_message()
cc.latActive = lat_active
cc.latActive = lat_active_state
mv2 = log.ModelDataV2.new_message()
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
lc = log.LiveCalibrationData.new_message()
lc.rpyCalib = [0.0, 0.0, 0.0]
return {
'carState': cs, 'selfdriveState': ss, 'carControl': cc,
'modelV2': mv2, 'liveCalibration': lc, 'driverStateV2': make_msg(False),
ds = make_msg(False)
sm = {
'carState': cs,
'selfdriveState': ss,
'carControl': cc,
'modelV2': mv2,
'liveCalibration': lc,
'driverStateV2': ds
}
driver_monitoring = DriverMonitoring()
@pytest.mark.parametrize("selfdrive_enabled, lat_active, steering, gas, expected_op_engaged, expected_driver_engaged", [
(False, False, False, False, False, False), # disabled
(True, False, False, False, True, False), # OP enabled
(False, True, False, False, True, False), # MADS lat-only
(True, True, False, False, True, False), # both active
(False, True, False, True, True, False), # MADS lat-only + gas
(True, True, False, True, True, True), # full op + gas: override
(False, True, True, False, True, True), # MADS lat-only + wheel touch: override
])
def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas,
expected_op_engaged, expected_driver_engaged):
sm = _build_sm(selfdrive_enabled, lat_active, steering, gas)
dm = DriverMonitoring()
captured = {}
orig = dm._update_events
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
captured_args = []
original_update_events = driver_monitoring._update_events
def spy(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
captured['driver_engaged'] = driver_engaged
captured['op_engaged'] = op_engaged
return orig(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear):
captured_args.append(op_engaged)
return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear)
dm._update_events = spy
dm.run_step(sm, demo=False)
assert captured['op_engaged'] == expected_op_engaged
assert captured['driver_engaged'] == expected_driver_engaged
driver_monitoring._update_events = spy_update_events
driver_monitoring.run_step(sm, demo=False)
# Assertion
assert len(captured_args) == 1, "Expected _update_events to be called exactly once"
actual_enabled = captured_args[0]
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"

View File

@@ -45,6 +45,8 @@ LaneChangeDirection = log.LaneChangeDirection
EventName = log.OnroadEvent.EventName
ButtonType = car.CarState.ButtonEvent.Type
SafetyModel = car.CarParams.SafetyModel
AlertLevel = log.DriverMonitoringState.AlertLevel
MonitoringPolicy = log.DriverMonitoringState.MonitoringPolicy
TurnDirection = custom.ModelDataV2SP.TurnDirection
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
@@ -140,6 +142,8 @@ class SelfdriveD(CruiseHelper):
self.params
)
self.recalibrating_seen = False
self.dm_lockout_set = False
self.dm_uncertain_alerted = False
self.state_machine = StateMachine()
self.rk = Ratekeeper(100, print_delay_threshold=None)
@@ -216,8 +220,27 @@ class SelfdriveD(CruiseHelper):
if not self.CP.pcmCruise and CS.vCruise > 250 and resume_pressed:
self.events.add(EventName.resumeBlocked)
# Handle DM
if not self.CP.notCar:
self.events.add_from_msg(self.sm['driverMonitoringState'].events)
# Block engaging until ignition cycle after max number or time of distractions
if self.sm['driverMonitoringState'].lockout and not self.dm_lockout_set:
self.params.put_bool_nonblocking("DriverTooDistracted", True)
self.dm_lockout_set = True
# No entry conditions
if self.sm['driverMonitoringState'].lockout or self.sm['driverMonitoringState'].alwaysOnLockout:
self.events.add(EventName.tooDistracted)
# Alerts
vision_dm = self.sm['driverMonitoringState'].activePolicy == MonitoringPolicy.vision
if self.sm['driverMonitoringState'].alertLevel == AlertLevel.one:
self.events.add(EventName.driverDistracted1 if vision_dm else EventName.driverUnresponsive1)
elif self.sm['driverMonitoringState'].alertLevel == AlertLevel.two:
self.events.add(EventName.driverDistracted2 if vision_dm else EventName.driverUnresponsive2)
elif self.sm['driverMonitoringState'].alertLevel == AlertLevel.three:
self.events.add(EventName.driverDistracted3 if vision_dm else EventName.driverUnresponsive3)
# Warn consistent DM uncertainty
if self.sm['driverMonitoringState'].visionPolicyState.uncertainOffroadAlertPercent >= 100 and not self.dm_uncertain_alerted:
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
self.dm_uncertain_alerted = True
self.events_sp.add_from_msg(self.sm['longitudinalPlanSP'].events)
# Add car events, ignore if CAN isn't valid
@@ -241,7 +264,7 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.pedalPressed)
# Create events for temperature, disk space, and memory
if self.sm['deviceState'].thermalStatus >= ThermalStatus.red:
if self.sm['deviceState'].thermalStatus >= ThermalStatus.overheated:
self.events.add(EventName.overheat)
if self.sm['deviceState'].freeSpacePercent < 7 and not SIMULATION:
self.events.add(EventName.outOfSpace)
@@ -298,16 +321,9 @@ class SelfdriveD(CruiseHelper):
# Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
direction = self.sm['modelV2'].meta.laneChangeDirection
mdv2sp = self.sm['modelDataV2SP']
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)
elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \
(mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right):
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
else:
if direction == LaneChangeDirection.left:
self.events.add(EventName.preLaneChangeLeft)

View File

@@ -449,9 +449,6 @@ def migrate_sensorEvents(msgs):
m.logMonoTime = msg.logMonoTime
m_dat = getattr(m, sensor_service)
m_dat.version = evt.version
m_dat.sensor = evt.sensor
m_dat.type = evt.type
m_dat.source = evt.source
m_dat.timestamp = evt.timestamp
setattr(m_dat, evt.which(), getattr(evt, evt.which()))
@@ -484,22 +481,41 @@ def migrate_onroadEvents(msgs):
return ops, [], []
@migration(inputs=["driverMonitoringState"])
@migration(inputs=["driverMonitoringStateDEPRECATED"])
def migrate_driverMonitoringState(msgs):
ops = []
for index, msg in msgs:
msg = msg.as_builder()
events = []
for event in msg.driverMonitoringState.deprecated.events:
try:
if not str(event.name).endswith('DEPRECATED'):
migrated_event = migrate_onroad_event(event)
if migrated_event is not None:
events.append(migrated_event)
except RuntimeError: # Member was null
traceback.print_exc()
old = msg.driverMonitoringStateDEPRECATED
new_msg = messaging.new_message('driverMonitoringState', valid=msg.valid, logMonoTime=msg.logMonoTime)
dm = new_msg.driverMonitoringState
dm.isRHD = old.isRHD
dm.activePolicy = log.DriverMonitoringState.MonitoringPolicy.vision if old.isActiveMode else \
log.DriverMonitoringState.MonitoringPolicy.wheeltouch
msg.driverMonitoringState.events = events
ops.append((index, msg.as_reader()))
AlertLevel = log.DriverMonitoringState.AlertLevel
event_to_alert_level = {
'driverDistracted1': AlertLevel.one, 'driverUnresponsive1': AlertLevel.one,
'driverDistracted2': AlertLevel.two, 'driverUnresponsive2': AlertLevel.two,
'driverDistracted3': AlertLevel.three, 'driverUnresponsive3': AlertLevel.three,
'tooDistracted': AlertLevel.three,
}
for event in old.events:
level = event_to_alert_level.get(str(event.name))
if level is not None:
dm.alertLevel = level
break
dm.visionPolicyState.awarenessPercent = int(max(0, min(100, (old.awarenessStatus if old.isActiveMode else old.awarenessActive) * 100)))
dm.visionPolicyState.awarenessStep = old.stepChange if old.isActiveMode else 0.
dm.visionPolicyState.isDistracted = old.isDistracted
dm.visionPolicyState.faceDetected = old.faceDetected
dm.visionPolicyState.pose.pitchCalib.offset = old.posePitchOffset
dm.visionPolicyState.pose.pitchCalib.calibratedPercent = int(min(100, old.posePitchValidCount / 600 * 100))
dm.visionPolicyState.pose.yawCalib.offset = old.poseYawOffset
dm.visionPolicyState.pose.yawCalib.calibratedPercent = int(min(100, old.poseYawValidCount / 600 * 100))
dm.visionPolicyState.pose.calibrated = old.posePitchValidCount >= 600 and old.poseYawValidCount >= 600
dm.wheeltouchPolicyState.awarenessPercent = int(max(0, min(100, (old.awarenessPassive if old.isActiveMode else old.awarenessStatus) * 100)))
dm.wheeltouchPolicyState.awarenessStep = 0. if old.isActiveMode else old.stepChange
ops.append((index, new_msg.as_reader()))
return ops, [], []

View File

@@ -35,7 +35,7 @@ GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
EXEC_TIMINGS = [
# model, instant max, average max
("modelV2", 0.05, 0.028),
("driverStateV2", 0.05, 0.016),
("driverStateV2", 0.05, 0.018),
]
def get_log_fn(test_route, ref="master"):

View File

View File

@@ -0,0 +1,278 @@
from dataclasses import dataclass
from enum import Enum
import time
class AnimationMode(Enum):
ONCE_FORWARD = 1
ONCE_FORWARD_BACKWARD = 2
REPEAT_FORWARD = 3
REPEAT_FORWARD_BACKWARD = 4
@dataclass
class Animation:
frames: list[list[tuple[int, int]]]
starting_frames: list[list[tuple[int, int]]] | None = None # played once before the main loop
frame_duration: float = 0.15 # seconds each frame is shown
mode: AnimationMode = AnimationMode.REPEAT_FORWARD_BACKWARD
repeat_interval: float = 5.0 # seconds between animation restarts (only for REPEAT modes)
hold_end: float = 0.0 # seconds to hold the last frame before playing backward (only for *_BACKWARD modes)
left_turn_remove: list[tuple[int, int]] | None = None # dots to remove from frame when turning left
right_turn_remove: list[tuple[int, int]] | None = None # dots to remove from frame when turning right
# --- Animation Helper Functions ---
def _mirror(dots: list[tuple[int, int]]) -> list[tuple[int, int]]:
"""Mirror a component from the left side of the face to the right"""
return [(r, 15 - c) for r, c in dots]
def _mirror_no_flip(dots: list[tuple[int, int]]) -> list[tuple[int, int]]:
"""Move a component to the mirrored position on the right half without flipping its shape."""
min_c = min(c for _, c in dots)
max_c = max(c for _, c in dots)
return [(r, 15 - max_c - min_c + c) for r, c in dots]
def _shift(dots: list[tuple[int, int]], rc: tuple[int, int]) -> list[tuple[int, int]]:
dr, dc = rc
return [(r + dr, c + dc) for r, c in dots]
def _make_frame(left_eye: list[tuple[int, int]], right_eye: list[tuple[int, int]],
left_brow: list[tuple[int, int]], right_brow: list[tuple[int, int]],
mouth: list[tuple[int, int]]) -> list[tuple[int, int]]:
return left_eye + left_brow + right_eye + right_brow + mouth
# --- Animation Helper Components ---
# Eyes (left side)
EYE_OPEN = [
(2, 2), (2, 3),
(3, 1), (3, 2), (3, 3), (3, 4),
(4, 1), (4, 2), (4, 3), (4, 4),
(5, 2), (5, 3)
]
EYE_HALF = [
(4, 1), (4, 2), (4, 3), (4, 4),
(5, 2), (5, 3)
]
EYE_CLOSED = [
(4, 1), (4, 4),
(5, 2), (5, 3),
]
EYE_LEFT_LOOK = [
(2, 2), (2, 3),
(3, 1), (3, 2),
(4, 1), (4, 2),
(5, 2), (5, 3),
]
EYE_RIGHT_LOOK = [
(2, 2), (2, 3),
(3, 3), (3, 4),
(4, 3), (4, 4),
(5, 2), (5, 3),
]
# Eyebrows (left side)
BROW_HIGH = [
(0, 1), (0, 2),
(1, 0),
]
BROW_LOWERED = [
(1, 1), (1, 2),
(2, 0)
]
BROW_STRAIGHT = [(1, 0), (1, 1), (1, 2)]
BROW_DOWN = [
(0, 1), (0, 2),
(1, 3)
]
# Mouths (centered, not mirrored)
MOUTH_SMILE = [
(6, 6), (6, 9),
(7, 7), (7, 8),
]
MOUTH_NORMAL = [(7, 7), (7, 8)]
MOUTH_SAD = [
(6, 7), (6, 8),
(7, 6), (7, 9)
]
# --- Animations ---
NORMAL = Animation(
frames=[
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_HALF, _mirror(EYE_HALF), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), BROW_LOWERED, _mirror(BROW_LOWERED), MOUTH_SMILE),
],
left_turn_remove=[
(3, 3), (3, 4),
(4, 3), (4, 4),
] + _mirror_no_flip([
(3, 1), (3, 2),
(4, 1), (4, 2),
]),
right_turn_remove=[
(3, 1), (3, 2),
(4, 1), (4, 2),
] + _mirror_no_flip([
(3, 3), (3, 4),
(4, 3), (4, 4),
])
)
ASLEEP = Animation(
frames=[
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), [], [], MOUTH_NORMAL),
],
)
SLEEPY = Animation(
frames=[
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), _shift(BROW_STRAIGHT, (1, 0)), [], MOUTH_NORMAL),
_make_frame(EYE_HALF, _mirror(EYE_CLOSED), BROW_LOWERED, [], MOUTH_NORMAL),
_make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, [], MOUTH_NORMAL)
],
frame_duration=0.25,
mode=AnimationMode.ONCE_FORWARD_BACKWARD,
repeat_interval=10,
hold_end=1.5,
)
INQUISITIVE = Animation(
frames=[
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_LEFT_LOOK, _mirror(EYE_RIGHT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_LEFT_LOOK, _mirror(EYE_RIGHT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_RIGHT_LOOK, _mirror(EYE_LEFT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_RIGHT_LOOK, _mirror(EYE_LEFT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
],
mode=AnimationMode.REPEAT_FORWARD,
frame_duration=0.15,
repeat_interval=10
)
WINK = Animation(
frames=[
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, _mirror(_shift(BROW_DOWN, (0, 2))), MOUTH_SMILE),
],
mode=AnimationMode.ONCE_FORWARD_BACKWARD,
frame_duration=0.75,
)
# --- Face Animator Class ---
class FaceAnimator:
def __init__(self, animation: Animation):
self._animation = animation
self._next: Animation | None = None
self._start_time = time.monotonic()
self._rewinding = False
self._rewind_start: float = 0.0
self._rewind_from: int = 0
self._seen_nonzero = False
def set_animation(self, animation: Animation):
if animation is not self._animation:
self._next = animation
def get_dots(self) -> list[tuple[int, int]]:
now = time.monotonic()
elapsed = now - self._start_time
# Handle rewind for forward-only animations
if self._rewinding:
rewind_elapsed = now - self._rewind_start
frames_back = round(rewind_elapsed / self._animation.frame_duration)
frame_index = self._rewind_from - frames_back
if frame_index <= 0:
return self._switch_to_next(now)
return self._animation.frames[frame_index]
# Play starting frames first (once)
starting = self._animation.starting_frames or []
starting_duration = len(starting) * self._animation.frame_duration
if starting and elapsed < starting_duration:
frame_index = min(int(elapsed / self._animation.frame_duration), len(starting) - 1)
return starting[frame_index]
# Main loop
loop_elapsed = elapsed - starting_duration if starting else elapsed
frame_index = _get_frame_index(self._animation, loop_elapsed, gap_first=bool(starting))
if frame_index != 0:
self._seen_nonzero = True
if self._next is not None:
if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero):
return self._switch_to_next(now)
# No natural return to frame 0 — start rewinding
if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD):
self._rewinding = True
self._rewind_start = now
self._rewind_from = frame_index
return self._animation.frames[frame_index]
def _switch_to_next(self, now: float) -> list[tuple[int, int]]:
self._animation = self._next
self._next = None
self._rewinding = False
self._seen_nonzero = False
self._start_time = now
return self._animation.frames[0]
def _get_frame_index(animation: Animation, elapsed: float, gap_first: bool = False) -> int:
"""Get the current frame index given elapsed time and animation mode."""
num_frames = len(animation.frames)
if num_frames == 1:
return 0
fd = animation.frame_duration
has_backward = animation.mode in (AnimationMode.ONCE_FORWARD_BACKWARD, AnimationMode.REPEAT_FORWARD_BACKWARD)
repeats = animation.mode in (AnimationMode.REPEAT_FORWARD, AnimationMode.REPEAT_FORWARD_BACKWARD)
forward_duration = num_frames * fd
backward_frames = max(num_frames - 2, 0) if has_backward else 0
hold = animation.hold_end if has_backward else 0.0
cycle_duration = forward_duration + hold + backward_frames * fd
if not repeats:
t = min(elapsed, cycle_duration)
else:
t = (elapsed + cycle_duration if gap_first else elapsed) % animation.repeat_interval
# Forward phase
if t < forward_duration:
return min(int(t / fd), num_frames - 1)
t -= forward_duration
# Hold at last frame
if t < hold:
return num_frames - 1
t -= hold
# Backward phase
if backward_frames and t < backward_frames * fd:
return num_frames - 2 - min(int(t / fd), backward_frames - 1)
return 0 if has_backward else num_frames - 1

View File

View File

@@ -0,0 +1,93 @@
import time
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.body.animations import FaceAnimator, ASLEEP, INQUISITIVE, NORMAL, SLEEPY
GRID_COLS = 16
GRID_ROWS = 8
DOT_RADIUS = 50 if gui_app.big_ui() else 10
IDLE_TIMEOUT = 30.0 # seconds of no joystick input before playing INQUISITIVE
IDLE_STEER_THRESH = 0.5 # degrees — below this counts as no input
IDLE_SPEED_THRESH = 0.01 # m/s — below this counts as no input
# This class is used both in BIG (tizi) and small (mici) UIs
class BodyLayout(Widget):
def __init__(self):
super().__init__()
self._animator = FaceAnimator(ASLEEP)
self._turning_left = False
self._turning_right = False
self._last_input_time = time.monotonic()
self._was_active = False
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
grid_w = (GRID_COLS - 1) * spacing
grid_h = (GRID_ROWS - 1) * spacing
offset_x = rect.x + (rect.width - grid_w) / 2
offset_y = rect.y + (rect.height - grid_h) / 2
for row, col in dots:
x = int(offset_x + col * spacing)
y = int(offset_y + row * spacing)
rl.draw_circle(x, y, DOT_RADIUS, color)
def _update_state(self):
super()._update_state()
sm = ui_state.sm
if ui_state.is_onroad():
if not self._was_active:
self._last_input_time = time.monotonic()
self._was_active = True
cs = sm['carState']
has_input = abs(cs.steeringAngleDeg) > IDLE_STEER_THRESH or abs(cs.vEgo) > IDLE_SPEED_THRESH
if has_input:
self._last_input_time = time.monotonic()
if time.monotonic() - self._last_input_time > IDLE_TIMEOUT:
self._animator.set_animation(INQUISITIVE)
else:
self._animator.set_animation(NORMAL)
else:
self._was_active = False
self._animator.set_animation(ASLEEP)
steer = sm['testJoystick'].axes[1] if len(sm['testJoystick'].axes) > 1 else 0
self._turning_left = steer <= -0.05
self._turning_right = steer >= 0.05
# play animation on screen tap
def _handle_mouse_release(self, mouse_pos):
super()._handle_mouse_release(mouse_pos)
if not self._was_active:
self._animator.set_animation(SLEEPY)
def _render(self, rect: rl.Rectangle):
dots = self._animator.get_dots()
animation = self._animator._animation
if self._turning_left and animation.left_turn_remove:
remove_set = set(animation.left_turn_remove)
dots = [d for d in dots if d not in remove_set]
elif self._turning_right and animation.right_turn_remove:
remove_set = set(animation.right_turn_remove)
dots = [d for d in dots if d not in remove_set]
self.draw_dot_grid(rect, dots, rl.WHITE)
if ui_state.is_offroad():
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
upper_half = rl.Rectangle(rect.x, rect.y, rect.width, rect.height / 2)
self._offroad_label.render(upper_half)

View File

@@ -2,13 +2,14 @@ import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
@@ -31,7 +32,9 @@ class MainLayout(Widget):
self._prev_onroad = False
# Initialize layouts
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
self._home_layout = HomeLayout()
self._home_body_layout = BodyLayout()
self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
self._content_rect = rl.Rectangle(0, 0, 0, 0)
@@ -57,14 +60,18 @@ class MainLayout(Widget):
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
for layout in (self._layouts[MainState.ONROAD], self._home_body_layout):
layout.set_click_callback(self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state)
ui_state.add_on_body_changed_callbacks(self._on_body_changed)
def _update_layout_rects(self):
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
self._content_rect = rl.Rectangle(self._rect.x + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
def _handle_onroad_transition(self):
if ui_state.started != self._prev_onroad:
@@ -73,6 +80,12 @@ class MainLayout(Widget):
self._set_mode_for_state()
def _set_mode_for_state(self):
# Don't go onroad if body, home is onroad
if ui_state.is_body:
self._set_current_layout(MainState.HOME)
self._sidebar.set_visible(not ui_state.ignition)
return
if ui_state.started:
# Don't hide sidebar from interactive timeout
if self._current_mode != MainState.ONROAD:
@@ -104,6 +117,10 @@ class MainLayout(Widget):
def _on_onroad_clicked(self):
self._sidebar.set_visible(not self._sidebar.is_visible)
def _on_body_changed(self):
self._layouts[MainState.HOME] = self._home_body_layout if ui_state.is_body else self._home_layout
self._set_mode_for_state()
def _render_main_content(self):
# Render sidebar
if self._sidebar.is_visible:

View File

@@ -36,7 +36,7 @@ class DeveloperLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._is_release = False # self._params.get_bool("IsReleaseBranch")
self._is_release = self._params.get_bool("IsReleaseBranch")
# Build items and keep references for callbacks/state updates
self._adb_toggle = toggle_item(
@@ -135,12 +135,6 @@ class DeveloperLayout(Widget):
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
if not long_man_enabled:
self._long_maneuver_toggle.action_item.set_state(False)
self._params.put_bool("LongitudinalManeuverMode", False)
lat_man_enabled = ui_state.is_offroad()
self._lat_maneuver_toggle.action_item.set_enabled(lat_man_enabled)
else:
self._long_maneuver_toggle.action_item.set_enabled(False)
self._lat_maneuver_toggle.action_item.set_enabled(False)

View File

@@ -1,7 +1,7 @@
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text
@@ -65,12 +65,13 @@ class FirehoseLayout(FirehoseLayoutBase):
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
y += 20 + 20
# TODO: add back once reliable
# Contribution count (if available)
if self._segment_count > 0:
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20 + 20
#if self._segment_count > 0:
# contrib_text = trn("{} segment of your driving is in the training dataset so far.",
# "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
# y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
# y += 20 + 20
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)

View File

@@ -42,7 +42,7 @@ class TogglesLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._is_release = False # self._params.get_bool("IsReleaseBranch")
self._is_release = self._params.get_bool("IsReleaseBranch")
# param, title, desc, icon, needs_restart
self._toggle_defs = {

View File

@@ -125,10 +125,8 @@ class Sidebar(Widget, SidebarSP):
def _update_temperature_status(self, device_state):
thermal_status = device_state.thermalStatus
if thermal_status == ThermalStatus.green:
if thermal_status == ThermalStatus.ok:
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
elif thermal_status == ThermalStatus.yellow:
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING)
else:
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)

View File

@@ -130,6 +130,7 @@ class MiciHomeLayout(Widget):
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
self._alerts_pill = AlertsPill()
@@ -137,6 +138,7 @@ class MiciHomeLayout(Widget):
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(),
self._experimental_icon,
self._body_icon,
self._mic_icon,
], spacing=18)
@@ -247,6 +249,7 @@ class MiciHomeLayout(Widget):
# ***** Center-aligned bottom section icons *****
self._experimental_icon.set_visible(self._experimental_mode)
self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(ui_state.is_body)
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
self._status_bar_layout.render(footer_rect)

View File

@@ -6,6 +6,7 @@ from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.application import gui_app
@@ -31,22 +32,25 @@ class MiciMainLayout(Scroller):
self._home_layout = MiciHomeLayout()
self._alerts_layout = MiciOffroadAlerts()
self._settings_layout = SettingsLayout()
self._onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._body_onroad_layout = BodyLayout()
# Initialize widget rects
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout):
for widget in (self._home_layout, self._alerts_layout, self._settings_layout,
self._car_onroad_layout, self._body_onroad_layout):
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._scroller.add_widgets([
self._alerts_layout,
self._home_layout,
self._onroad_layout,
self._car_onroad_layout,
self._body_onroad_layout,
])
self._scroller.set_reset_scroll_at_show(False)
# Disable scrolling when onroad is interacting with bookmark
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())
self._scroller.set_scrolling_enabled(lambda: not self._car_onroad_layout.is_swiping_left())
# Set callbacks
self._setup_callbacks()
@@ -59,14 +63,22 @@ class MiciMainLayout(Scroller):
if not self._onboarding_window.completed:
gui_app.push_widget(self._onboarding_window)
@property
def _onroad_layout(self) -> Widget:
# For scroll_to
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
def _setup_callbacks(self):
self._home_layout.set_callbacks(
on_settings=lambda: gui_app.push_widget(self._settings_layout),
on_alerts=lambda: self._scroll_to(self._alerts_layout),
alert_count_callback=self._alerts_layout.active_alerts,
)
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
for layout in (self._car_onroad_layout, self._body_onroad_layout):
layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._on_interactive_timeout)
ui_state.add_on_body_changed_callbacks(self._on_body_changed)
def _scroll_to(self, layout: Widget):
layout_x = int(layout.rect.x)
@@ -132,3 +144,7 @@ class MiciMainLayout(Scroller):
user_bookmark = messaging.new_message('bookmarkButton')
user_bookmark.valid = True
self._pm.send('bookmarkButton', user_bookmark)
def _on_body_changed(self):
self._car_onroad_layout.set_visible(not ui_state.is_body)
self._body_onroad_layout.set_visible(ui_state.is_body)

View File

@@ -140,7 +140,7 @@ class TrainingGuideDMTutorial(NavWidget):
# stay at 100% once reached
in_bad_face = gui_app.get_active_widget() == self._bad_face_page
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
if ((dm_state.visionPolicyState.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
slow = self._progress.x < 0.25
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
self._progress.x += 1.0 / (duration * gui_app.target_fps)

View File

@@ -131,12 +131,6 @@ class DeveloperLayoutMici(NavScroller):
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
self._long_maneuver_toggle.set_enabled(long_man_enabled)
if not long_man_enabled:
self._long_maneuver_toggle.set_checked(False)
ui_state.params.put_bool("LongitudinalManeuverMode", False)
lat_man_enabled = ui_state.is_offroad()
self._lat_maneuver_toggle.set_enabled(lat_man_enabled)
else:
self._long_maneuver_toggle.set_enabled(False)
self._lat_maneuver_toggle.set_enabled(False)

View File

@@ -1,20 +1,15 @@
import pyray as rl
from cereal import log, messaging
from cereal import car, log, messaging
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.selfdrive.selfdrived.events import EVENTS, ET
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.label import gui_label
EventName = log.OnroadEvent.EventName
EVENT_TO_INT = EventName.schema.enumerants
class DriverCameraView(CameraView):
def _calc_frame_matrix(self, rect: rl.Rectangle):
@@ -107,11 +102,14 @@ class BaseDriverCameraDialog(Widget):
if self._pm is None:
return
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
ALERT_SOUNDS = {
'two': AudibleAlert.promptDistracted,
'three': AudibleAlert.warningImmediate,
}
msg = messaging.new_message('selfdriveState')
if dm_state is not None and len(dm_state.events):
event_name = EVENT_TO_INT[dm_state.events[0].name]
if event_name is not None and event_name in EVENTS and ET.PERMANENT in EVENTS[event_name]:
msg.selfdriveState.alertSound = EVENTS[event_name][ET.PERMANENT].audible_alert
if dm_state is not None:
msg.selfdriveState.alertSound = ALERT_SOUNDS.get(str(dm_state.alertLevel), AudibleAlert.none)
self._pm.send('selfdriveState', msg)
def _render_dm_alerts(self, rect: rl.Rectangle):
@@ -119,29 +117,31 @@ class BaseDriverCameraDialog(Widget):
dm_state = ui_state.sm["driverMonitoringState"]
self._publish_alert_sound(dm_state)
is_vision = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
f"Awareness: {dm_state.awarenessStatus * 100:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
color=rl.Color(0, 0, 0, 180))
gui_label(rect, f"Awareness: {dm_state.awarenessStatus * 100:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
color=rl.Color(255, 255, 255, int(255 * 0.9)))
if not dm_state.events:
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
return
# Show first event (only one should be active at a time)
event_name_str = str(dm_state.events[0].name).split('.')[-1]
# Show alert level
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
color=rl.Color(0, 0, 0, 180))
gui_label(rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
color=rl.Color(255, 255, 255, int(255 * 0.9)))
@@ -156,7 +156,7 @@ class BaseDriverCameraDialog(Widget):
def _draw_face_detection(self, rect: rl.Rectangle):
dm_state = ui_state.sm["driverMonitoringState"]
driver_data = self.driver_state_renderer.get_driver_data()
if not dm_state.faceDetected:
if not dm_state.visionPolicyState.faceDetected:
return
# Get face position and orientation

View File

@@ -6,7 +6,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.monitoring.helpers import face_orientation_from_net
AlertSize = log.SelfdriveState.AlertSize
@@ -35,6 +35,8 @@ class DriverStateRenderer(Widget):
self._is_active = False
self._is_rhd = False
self._face_detected = False
self._face_pitch = 0.
self._face_yaw = 0.
self._should_draw = False
self._force_active = False
self._looking_center = False
@@ -153,9 +155,11 @@ class DriverStateRenderer(Widget):
sm = ui_state.sm
dm_state = sm["driverMonitoringState"]
self._is_active = dm_state.isActiveMode
self._is_active = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
self._is_rhd = dm_state.isRHD
self._face_detected = dm_state.faceDetected
self._face_detected = dm_state.visionPolicyState.faceDetected
self._face_pitch = dm_state.visionPolicyState.pose.pitch + math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
self._face_yaw = -dm_state.visionPolicyState.pose.yaw # undo sign flip in face_orientation_from_model to match UI convention
driverstate = sm["driverStateV2"]
driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData
@@ -163,26 +167,9 @@ class DriverStateRenderer(Widget):
def _update_state(self):
# Get monitoring state
driver_data = self.get_driver_data()
driver_orient = driver_data.faceOrientation
driver_position = driver_data.facePosition
if len(driver_orient) != 3:
return
# Calibrate orientation so looking straight ahead at road (instead of at device) is (0, 0, 0)
sm = ui_state.sm
if sm.valid['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) == 3:
cal_rpy = sm['liveCalibration'].rpyCalib
else:
cal_rpy = [0.0, 0.0, 0.0]
_, pitch, yaw = face_orientation_from_net(driver_orient, driver_position, cal_rpy)
pitch += math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
yaw = -yaw # undo sign flip in face_orientation_from_net to match UI convention
pitch = self._pitch_filter.update(pitch)
yaw = self._yaw_filter.update(yaw)
_ = self.get_driver_data()
pitch = self._pitch_filter.update(self._face_pitch)
yaw = self._yaw_filter.update(self._face_yaw)
# hysteresis on looking center
if abs(pitch) < LOOKING_CENTER_THRESHOLD_LOWER and abs(yaw) < LOOKING_CENTER_THRESHOLD_LOWER:

View File

@@ -114,7 +114,7 @@ class DriverStateRenderer(Widget):
# Get monitoring state
dm_state = sm["driverMonitoringState"]
self.is_active = dm_state.isActiveMode
self.is_active = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
self.is_rhd = dm_state.isRHD
# Update fade state (smoother transition between active/inactive)

View File

@@ -10,7 +10,6 @@ import time
import pyray as rl
from cereal import custom
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.lib.multilang import tr
@@ -208,7 +207,7 @@ class ModelsLayout(Widget):
for bundle in bundles:
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{DEFAULT_MODEL} (Default)", 'short_name': "Default"})])]
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': tr("Default Model"), 'short_name': "Default"})])]
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
@@ -244,7 +243,7 @@ class ModelsLayout(Widget):
self._update_lagd_description(live_delay)
self.model_manager = ui_state.sm["modelManagerSP"]
self._handle_bundle_download_progress()
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else tr("Default Model")
self.current_model_item.action_item.set_value(active_name)
if not ui_state.is_offroad():

View File

@@ -120,12 +120,20 @@ class SteeringLayout(Widget):
def _update_state(self):
super()._update_state()
torque_allowed = ui_state.CP is not None and ui_state.CP.steerControlType != car.CarParams.SteerControlType.angle
torque_allowed = True
if ui_state.CP is not None:
mads_main_desc = self._mads_limited_desc if self._mads_settings_layout._mads_limited_settings() else self._mads_full_desc
self._mads_toggle.set_description(f"<b>{mads_main_desc}</b><br><br>{self._mads_base_desc}")
if ui_state.CP.steerControlType == car.CarParams.SteerControlType.angle:
ui_state.params.remove("EnforceTorqueControl")
ui_state.params.remove("NeuralNetworkLateralControl")
torque_allowed = False
else:
self._mads_toggle.set_description(f"<b>{self._mads_check_compat_desc}</b><br><br>{self._mads_base_desc}")
ui_state.params.remove("EnforceTorqueControl")
ui_state.params.remove("NeuralNetworkLateralControl")
torque_allowed = False
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())

View File

@@ -51,17 +51,11 @@ class LaneChangeSettingsLayout(Widget):
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."),
)
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 = [
self._lane_change_timer,
LineSeparatorSP(40),
self._bsm_delay,
self._road_edge_block,
]
return items

View File

@@ -8,7 +8,6 @@ from collections.abc import Callable
import pyray as rl
from cereal import custom
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
from openpilot.selfdrive.ui.ui_state import ui_state, device
@@ -28,8 +27,7 @@ class CurrentModelInfo(Widget):
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
max_width = int(self._rect.width - 20)
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
default_text = f"{DEFAULT_MODEL} (Default)".lower()
self.current_model_text = UnifiedLabel(default_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
self.current_model_text = UnifiedLabel(tr("default model"), 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
@@ -100,7 +98,7 @@ class ModelsLayoutMici(NavScroller):
folders = self._get_grouped_bundles(favorites)
folder_buttons = []
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
default_btn = BigButton(tr("default model"))
default_btn.set_click_callback(self._select_default)
folder_buttons.append(default_btn)
@@ -170,8 +168,7 @@ class ModelsLayoutMici(NavScroller):
self._was_downloading = is_downloading
self.current_model_info.current_model_header.set_text(tr("active model"))
model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else f"{DEFAULT_MODEL} (Default)".lower()
self.current_model_info.current_model_text.set_text(model_text)
self.current_model_info.current_model_text.set_text(manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else tr("default model"))
self.current_model_info.info_header.set_text(tr("cache size"))
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")

View File

@@ -5,9 +5,8 @@ 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.settings import settings as OP
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsBigButton
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
@@ -33,11 +32,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
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_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
sunnylink_btn = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
models_btn = BigButton("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))
# onroad: enable button sits at the front (left of toggles)

View File

@@ -141,8 +141,7 @@ class DeveloperUiRenderer(Widget):
# Add torque-specific elements if using torque control
if sm['controlsState'].lateralControlState.which() == 'torqueState':
override_active = ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled
if sm.valid['liveTorqueParameters'] or override_active:
if sm.valid['liveTorqueParameters']:
elements.extend([
self.friction_elem.update(sm, ui_state.is_metric),
self.lat_accel_factor_elem.update(sm, ui_state.is_metric),

View File

@@ -8,7 +8,8 @@ import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.text_measure import measure_text_cached
@@ -247,12 +248,12 @@ class FrictionCoefficientElement:
self.unit = ""
def update(self, sm, is_metric: bool) -> UiElement:
if ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled:
return UiElement(f"{ui_state.torque_override_friction:.3f}", "FRIC.", self.unit, rl.WHITE)
ltp = sm['liveTorqueParameters']
value = f"{ltp.frictionCoefficientFiltered:.3f}"
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
friction_coef = ltp.frictionCoefficientFiltered
live_valid = ltp.liveValid
value = f"{friction_coef:.3f}"
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
return UiElement(value, "FRIC.", self.unit, color)
@@ -261,12 +262,12 @@ class LatAccelFactorElement:
self.unit = ""
def update(self, sm, is_metric: bool) -> UiElement:
if ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled:
return UiElement(f"{ui_state.torque_override_lat_accel_factor:.3f}", "L.A.F.", self.unit, rl.WHITE)
ltp = sm['liveTorqueParameters']
value = f"{ltp.latAccelFactorFiltered:.3f}"
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
lat_accel_factor = ltp.latAccelFactorFiltered
live_valid = ltp.liveValid
value = f"{lat_accel_factor:.3f}"
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
return UiElement(value, "L.A.F.", self.unit, color)

View File

@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
"""
from enum import Enum
from cereal import messaging, log, car, custom
from cereal import messaging, log, custom
from openpilot.common.params import Params
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
@@ -26,20 +26,22 @@ class OnroadTimerStatus(Enum):
class UIStateSP:
def __init__(self):
self.params = Params()
self.CP_SP: custom.CarParamsSP | None = None
self.has_icbm: bool = False
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
self.params = Params()
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
]
self.sunnylink_state = SunnylinkState()
self.update_params_()
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = 0
self._sp_initialized: bool = False
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.reset_onroad_sleep_timer()
self.CP_SP: custom.CarParamsSP | None = None
self.has_icbm: bool = False
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
def update(self) -> None:
if self.sunnylink_enabled:
@@ -121,13 +123,11 @@ class UIStateSP:
return "disengaged"
def update_params(self) -> None:
def update_params_(self) -> None:
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self._enforce_constraints()
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo")
@@ -143,63 +143,11 @@ class UIStateSP:
self.standstill_timer = self.params.get_bool("StandstillTimer")
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
self.torque_bar = self.params.get_bool("TorqueBar")
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True))
self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True))
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
self.turn_signals = self.params.get_bool("ShowTurnSignals")
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
self.always_offroad = self.params.get_bool("OffroadMode")
if not self._sp_initialized:
self._sp_initialized = True
self.reset_onroad_sleep_timer()
def _enforce_constraints(self) -> None:
has_long = self.has_longitudinal_control
CP = self.CP
if CP is not None:
# Angle steering: no torque-based lateral controls
if CP.steerControlType == car.CarParams.SteerControlType.angle:
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
# Alpha longitudinal: clear if not available
if not CP.alphaLongitudinalAvailable:
self.params.remove("AlphaLongitudinalEnabled")
# BSM not available: clear BSM-dependent settings
if not CP.enableBsm:
self.params.remove("AutoLaneChangeBsmDelay")
else:
# No CarParams: clear all car-dependent params as safety default
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
self.params.remove("AlphaLongitudinalEnabled")
# No longitudinal control: no experimental mode or DEC
if not has_long:
self.params.remove("ExperimentalMode")
self.params.remove("DynamicExperimentalControl")
# ICBM: clear if not available or if full longitudinal control is active
if self.CP_SP is not None:
if not self.CP_SP.intelligentCruiseButtonManagementAvailable or has_long:
self.params.remove("IntelligentCruiseButtonManagement")
self.has_icbm = False
else:
self.params.remove("IntelligentCruiseButtonManagement")
self.has_icbm = False
# Cruise features requiring longitudinal or ICBM
if not (has_long or self.has_icbm):
self.params.remove("CustomAccIncrementsEnabled")
self.params.remove("SmartCruiseControlVision")
self.params.remove("SmartCruiseControlMap")
class DeviceSP:
@staticmethod
@@ -215,6 +163,7 @@ class DeviceSP:
if _ui_state.onroad_brightness_timer != 0:
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
return max(30.0, cur_brightness)
# For AUTO (Default) and Manual modes (while timer running), use standard brightness
return cur_brightness
# 0: Auto (Default), 1: Auto (Dark), 2: Screen Off

View File

@@ -15,6 +15,7 @@ from openpilot.system.hardware import HARDWARE, PC
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
PARAM_UPDATE_TIME = 5.0
class UIStatus(Enum):
@@ -59,6 +60,7 @@ class UIState(UIStateSP):
"carOutput",
"carControl",
"liveParameters",
"testJoystick",
"rawAudioData",
] + self.sm_services_ext
)
@@ -74,7 +76,7 @@ class UIState(UIStateSP):
# Core state variables
self.is_metric: bool = self.params.get_bool("IsMetric")
self.is_release = False # self.params.get_bool("IsReleaseBranch")
self.is_release = self.params.get_bool("IsReleaseBranch")
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
self.started: bool = False
self.ignition: bool = False
@@ -82,15 +84,15 @@ class UIState(UIStateSP):
self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown
self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard
self.has_longitudinal_control: bool = False
self.is_body: bool | None = None
self.CP: car.CarParams | None = None
self.light_sensor: float = -1.0
self._param_update_time: float = 0.0
self._param_update_time: float = -PARAM_UPDATE_TIME
# Callbacks
self._offroad_transition_callbacks: list[Callable[[], None]] = []
self._engaged_transition_callbacks: list[Callable[[], None]] = []
self.update_params()
self._on_body_changed_callbacks: list[Callable[[], None]] = []
def add_offroad_transition_callback(self, callback: Callable[[], None]):
self._offroad_transition_callbacks.append(callback)
@@ -98,6 +100,9 @@ class UIState(UIStateSP):
def add_engaged_transition_callback(self, callback: Callable[[], None]):
self._engaged_transition_callbacks.append(callback)
def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
self._on_body_changed_callbacks.append(callback)
@property
def engaged(self) -> bool:
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
@@ -113,7 +118,7 @@ class UIState(UIStateSP):
self.sm.update(0)
self._update_state()
self._update_status()
if time.monotonic() - self._param_update_time > 5.0:
if time.monotonic() - self._param_update_time >= PARAM_UPDATE_TIME:
self.update_params()
device.update()
UIStateSP.update(self)
@@ -188,7 +193,13 @@ class UIState(UIStateSP):
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
else:
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
UIStateSP.update_params(self)
if self.is_body != self.CP.notCar:
self.is_body = self.CP.notCar
for callback in self._on_body_changed_callbacks:
callback()
UIStateSP.update_params_(self)
self._param_update_time = time.monotonic()

View File

@@ -147,7 +147,6 @@ class ModularAssistiveDrivingSystem:
self.events.remove(EventName.speedTooLow)
self.events.remove(EventName.cruiseDisabled)
self.events.remove(EventName.manualRestart)
self.events.remove(EventName.espActive)
selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable)
set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents)

View File

@@ -1,5 +1,6 @@
import os
import glob
from tinygrad import Device
Import('env', 'arch')
lenv = env.Clone()
@@ -21,10 +22,19 @@ if PC:
if outputs:
lenv.Command(outputs, inputs, cmd)
tg_flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}',
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
available = set(Device.get_available_devices())
if 'CUDA' in available:
tg_backend = 'CUDA'
tg_flags = f'DEV={tg_backend}'
elif 'QCOM' in available:
tg_backend = 'QCOM'
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
else:
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
tg_flags = f'DEV={tg_backend} THREADS=0'
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
image_flag = {
'larch64': 'IMAGE=2',
@@ -38,7 +48,7 @@ def tg_compile(flags, model_name):
return lenv.Command(
out,
[fn + ".onnx"] + tinygrad_files,
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
)
# Compile models
@@ -46,9 +56,9 @@ for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'drivin
if File(f"models/{model_name}.onnx").exists():
tg_compile(tg_flags, model_name)
script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_modeld.py").abspath)]
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
compile_warp_cmd = f'{pythonpath_string} {tg_flags} python3 -m sunnypilot.modeld_v2.warp'
compile_warp_cmd = f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 -m sunnypilot.modeld_v2.warp'
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
warp_targets = []

View File

@@ -34,7 +34,6 @@ from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
@@ -247,9 +246,6 @@ def main(demo=False):
prev_action = log.ModelDataV2.Action()
DH = DesireHelper()
RELC = RoadEdgeLaneChangeController(DH)
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -353,10 +349,7 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs, v_ego)
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction

View File

@@ -2,8 +2,11 @@ import os
os.environ['DEV'] = 'CPU'
import pytest
import numpy as np
from openpilot.selfdrive.modeld.compile_warp import get_nv12_info, CAMERA_CONFIGS
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.warp import Warp
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
('img', 'big_img'),

View File

@@ -6,29 +6,61 @@ from tinygrad.tensor import Tensor
from tinygrad.engine.jit import TinyJit
from tinygrad.device import Device
# https://github.com/tinygrad/tinygrad/issues/15682
from tinygrad.uop.ops import UOp, Ops
_orig = UOp.__reduce__
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.modeld.compile_warp import (
CAMERA_CONFIGS, MEDMODEL_INPUT_SIZE, make_frame_prepare, make_update_both_imgs,
warp_pkl_path,
from openpilot.selfdrive.modeld.compile_modeld import (
NV12Frame, make_frame_prepare,
)
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
MODELS_DIR = Path(__file__).parent / 'models'
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
UPSTREAM_BUFFER_LENGTH = 5
def warp_pkl_path(cam_w, cam_h):
return MODELS_DIR / f'warp_{cam_w}x{cam_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
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):
def compile_v2_warp(cam_w, cam_h, buffer_length, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1]):
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
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)
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
frame_prepare = make_frame_prepare(nv12, 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()
@@ -62,9 +94,11 @@ def compile_v2_warp(cam_w, cam_h, buffer_length):
class Warp:
def __init__(self, buffer_length=2):
def __init__(self, buffer_length=2, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1]):
self.buffer_length = buffer_length
self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
self.model_w = model_w
self.model_h = model_h
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']}
@@ -92,8 +126,9 @@ class Warp:
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)
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
frame_prepare = make_frame_prepare(nv12, self.model_w, self.model_h)
update_both_imgs = make_update_both_imgs(frame_prepare, self.model_w, self.model_h)
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
if key not in self._nv12_cache:

View File

@@ -4,9 +4,8 @@ import hashlib
from openpilot.common.basedir import BASEDIR
from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "model_name.py")
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
@@ -26,7 +25,8 @@ def update_model_hash():
def get_current_default_model_name():
print("[GET DEFAULT MODEL NAME]")
name = DEFAULT_MODEL
with open(DEFAULT_MODEL_NAME_PATH) as f:
name = f.read().split('"')[1]
print(f'Current default model name: "{name}"')
return name
@@ -35,7 +35,7 @@ def get_current_default_model_name():
def update_default_model_name(name: str):
print("[CHANGE DEFAULT MODEL NAME]")
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
f.write(f'DEFAULT_MODEL = "{name}"\n')
f.write(f'#define DEFAULT_MODEL "{name}"\n')
print(f'New default model name: "{name}"')
print("[DONE]")
@@ -51,7 +51,7 @@ if __name__ == "__main__":
exit(0)
current_name = get_current_default_model_name()
new_name = args.new_name
new_name = f"{args.new_name} (Default)"
if current_name == new_name:
print(f'Proposed default model name: "{new_name}"')
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()

View File

@@ -116,7 +116,7 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v16.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v17.json"
def __init__(self, params: Params):
self.params = params

View File

@@ -1 +0,0 @@
DEFAULT_MODEL = "POP model"

View File

@@ -1 +1 @@
5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350
32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf

View File

@@ -1,84 +0,0 @@
"""
Copyright (c) 2021-, rav4kumar, 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.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
class RoadEdgeLaneChangeController:
def __init__(self, desire_helper):
self.DH = desire_helper
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) -> 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]
left_cond = left_edge_prob > EDGE_PROB and left_lane_prob < NEARSIDE_PROB and right_lane_prob >= left_lane_prob
right_cond = right_edge_prob > EDGE_PROB and right_lane_prob < NEARSIDE_PROB and left_lane_prob >= right_lane_prob
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

View File

@@ -5,8 +5,6 @@ from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
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.relc import RoadEdgeLaneChangeController
TurnDirection = custom.ModelDataV2SP.TurnDirection
@@ -109,11 +107,7 @@ def set_lane_turn_params():
])
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
dh = DesireHelper()
relc = RoadEdgeLaneChangeController(dh)
relc.enabled = True
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob,
left_edge_detected=relc.left_edge_detected, right_edge_detected=relc.right_edge_detected)
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

View File

@@ -1,99 +0,0 @@
"""
Copyright (c) 2021-, rav4kumar, 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 pytest
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.sunnypilot.selfdrive.controls.lib.relc import (
RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED,
)
V_HIGH = MIN_SPEED + 2.0
V_LOW = MIN_SPEED - 1.0
@pytest.fixture
def relc(mocker):
mock_params = mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
mock_params.return_value.get_bool.return_value = True
controller = RoadEdgeLaneChangeController(DesireHelper())
controller.enabled = True
return controller
def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH):
for _ in range(int(seconds / DT_MDL) + 1):
controller.update(road_edge_stds, lane_line_probs, v_ego)
@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)
relc.update(*edge, V_HIGH)
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)
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)
assert not relc.left_edge_detected
assert not relc.right_edge_detected

View File

@@ -255,7 +255,7 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData
}
// Gyro Uncalibrated
if (log.getSensor() == SENSOR_GYRO_UNCALIBRATED && log.getType() == SENSOR_TYPE_GYROSCOPE_UNCALIBRATED) {
if (log.which() == cereal::SensorEventData::GYRO_UNCALIBRATED) {
auto v = log.getGyroUncalibrated().getV();
auto meas = Vector3d(-v[2], -v[1], -v[0]);
@@ -273,7 +273,7 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData
}
// Accelerometer
if (log.getSensor() == SENSOR_ACCELEROMETER && log.getType() == SENSOR_TYPE_ACCELEROMETER) {
if (log.which() == cereal::SensorEventData::ACCELERATION) {
auto v = log.getAcceleration().getV();
// TODO: reduce false positives and re-enable this check

View File

@@ -19,7 +19,7 @@ if platform.system() == 'Darwin':
class TestLocationdProc:
LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'liveCalibration',
'accelerometer', 'gyroscope', 'magnetometer']
'accelerometer', 'gyroscope']
def setup_method(self):
self.pm = messaging.PubMaster(self.LLD_MSGS)

View File

@@ -4,6 +4,7 @@ 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 cereal import car
@@ -17,6 +18,7 @@ RELAXED_MIN_BUCKET_POINTS = np.array([1, 200, 300, 500, 500, 300, 200, 1])
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
class TorqueEstimatorExt:
def __init__(self, CP: car.CarParams):
self.CP = CP
@@ -26,7 +28,6 @@ class TorqueEstimatorExt:
self.enforce_torque_control_toggle = self._params.get_bool("EnforceTorqueControl") # only during init
self.use_params = self.CP.brand in ALLOWED_CARS and self.CP.lateralTuning.which() == 'torque'
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
self.custom_torque_params = self._params.get_bool("CustomTorqueParams")
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
self.min_bucket_points = RELAXED_MIN_BUCKET_POINTS
self.factor_sanity = 0.0
@@ -50,14 +51,13 @@ class TorqueEstimatorExt:
def _update_params(self):
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
self.custom_torque_params = self._params.get_bool("CustomTorqueParams")
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
def update_use_params(self):
self._update_params()
if self.enforce_torque_control_toggle:
if self.custom_torque_params and self.torque_override_enabled:
if self.torque_override_enabled:
self.use_params = False
else:
self.use_params = self.use_live_torque_params

View File

@@ -243,12 +243,4 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.normal, AlertSize.none,
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),
},
}

View File

@@ -18,7 +18,7 @@ import time
from jsonrpc import dispatcher
from functools import partial
from openpilot.common.params import Params, ParamKeyType
from openpilot.common.params import Params
from openpilot.common.realtime import set_core_affinity
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.hw import Paths
@@ -28,14 +28,11 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
create_connection, WebSocketConnectionClosedException)
import cereal.messaging as messaging
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
from openpilot.sunnypilot.selfdrive.car.sync_car_list_param import update_car_list_param
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import generate_schema
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://athena.sunnylink.ai')
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
LOCAL_PORT_WHITELIST = {8022}
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
@@ -47,15 +44,12 @@ params = Params()
# Parameters that should never be remotely modified
BLOCKED_PARAMS = {
"AdbEnabled",
"CompletedSunnylinkConsentVersion",
"CompletedTrainingVersion",
"GithubUsername", # Could grant SSH access
"GithubSshKeys", # Direct SSH key injection
"HasAcceptedTerms",
"HasAcceptedTermsSP",
"OnroadCycleRequested", # Prevent remote cycle trigger
"ParamsVersion", # Device-managed version counter
}
@@ -205,19 +199,34 @@ def getParamsAllKeysV1() -> dict[str, str]:
@dispatcher.add_method
def getParamsMetadata() -> str:
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
Reads settings_ui.json, injects live capabilities from CarParams, compresses,
and returns. Single RPC for the frontend to get the complete settings UI and
runtime capabilities.
"""
"""Compressed equivalent of getParamsAllKeysV1 — same struct, gzipped + base64."""
try:
schema = generate_schema()
schema["capabilities"] = generate_capabilities()
schema["capability_labels"] = CAPABILITY_LABELS
schema["default_model"] = DEFAULT_MODEL
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
with open(METADATA_PATH) as f:
metadata = json.load(f)
except Exception:
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
metadata = {}
try:
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
params_list: list[dict] = []
for key in available_keys:
value = get_param_as_byte(key, get_default=True)
param_entry: dict = {
"key": key,
"type": int(params.get_type(key).value),
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
}
if key in metadata:
param_entry["_extra"] = metadata[key]
params_list.append(param_entry)
raw = json.dumps(params_list, separators=(',', ':')).encode('utf-8')
return base64.b64encode(gzip.compress(raw)).decode('utf-8')
except Exception:
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
raise
@@ -229,25 +238,12 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
try:
zero_values: dict[int, bytes] = {
ParamKeyType.STRING.value: b"",
ParamKeyType.BOOL.value: b"0",
ParamKeyType.INT.value: b"0",
ParamKeyType.FLOAT.value: b"0.0",
ParamKeyType.TIME.value: b"",
ParamKeyType.JSON.value: b"{}",
ParamKeyType.BYTES.value: b"",
}
param_keys_validated = [key for key in params_keys if key in available_keys]
params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []}
for key in param_keys_validated:
value = get_param_as_byte(key)
if value is None:
value = get_param_as_byte(key, get_default=True)
if value is None:
param_type = params.get_type(key)
value = zero_values.get(param_type.value, b"")
continue
params_dict["params"].append({
"key": key,
@@ -278,13 +274,6 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N
except Exception as e:
cloudlog.error(f"sunnylinkd.saveParams.exception {e}")
# Increment version counter for frontend change detection
try:
current = int(params.get("ParamsVersion") or "0")
params.put("ParamsVersion", str(current + 1))
except Exception:
pass
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
sunnylink_dongle_id = params.get("SunnylinkDongleId")

View File

@@ -1,186 +0,0 @@
"""
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 json
from cereal import car, custom, messaging
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
# Wire-protocol version for the capabilities payload. Bump on breaking changes
# only; additive fields are backward-compatible and do not require a bump.
PROTOCOL_VERSION = 1
# All capability fields that rules may reference.
# Non-boolean fields must have defaults in CAPABILITY_DEFAULTS.
CAPABILITY_FIELDS = (
"protocol_version",
"has_longitudinal_control",
"has_icbm",
"icbm_available",
"torque_allowed",
"brand",
"pcm_cruise",
"alpha_long_available",
"steer_control_type",
"enable_bsm",
"is_release",
"is_sp_release",
"is_development",
"tesla_has_vehicle_bus",
"has_stop_and_go",
"stock_longitudinal",
"device_type",
"subaru_has_sng",
"hyundai_alpha_long_available",
)
CAPABILITY_LABELS: dict[str, str] = {
"protocol_version": "Capabilities protocol version",
"has_longitudinal_control": "sunnypilot longitudinal control",
"has_icbm": "ICBM enabled",
"icbm_available": "ICBM available",
"torque_allowed": "torque steering (not available for angle steering vehicles)",
"brand": "Vehicle brand",
"pcm_cruise": "PCM cruise",
"alpha_long_available": "Alpha Longitudinal available",
"steer_control_type": "Steer control type",
"enable_bsm": "BSM available",
"is_release": "Release branch",
"is_sp_release": "SP release branch",
"is_development": "Development branch",
"tesla_has_vehicle_bus": "Tesla vehicle bus",
"has_stop_and_go": "Stop and Go",
"stock_longitudinal": "stock longitudinal",
"device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
}
# Explicit defaults for non-boolean capability fields
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
"brand": "",
"steer_control_type": "",
"device_type": "",
"protocol_version": PROTOCOL_VERSION,
}
def _bundle_field(bundle: dict | None, key: str) -> str:
return bundle.get(key, "") if isinstance(bundle, dict) else ""
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
Bundle (manual car selection) is a pre-fingerprint approximation.
CarParams (auto-fingerprint) is the authoritative post-fingerprint source.
Mirrors the per-brand update_settings() logic in device UI layouts.
"""
brand = caps["brand"]
if brand == "hyundai":
if bundle_platform:
try:
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
except KeyError:
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
elif CP is not None:
caps["hyundai_alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
elif brand == "subaru":
if bundle_platform:
try:
flags = SUBARU_CAR[bundle_platform].config.flags
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
except KeyError:
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
elif CP is not None:
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
def generate_capabilities(params: Params | None = None) -> dict:
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
When CarPlatformBundle is present, brand and platform come from the bundle
(mirrors Raylib). CarParams* deserialization is the fallback before the bundle
is written (early after first pairing).
"""
params = params or Params()
caps: dict = {field: CAPABILITY_DEFAULTS.get(field, False) for field in CAPABILITY_FIELDS}
# Wire-protocol version is always set explicitly.
caps["protocol_version"] = PROTOCOL_VERSION
# Hardware + boolean params (no CarParams dependency)
caps["device_type"] = HARDWARE.get_device_type()
caps["is_release"] = False # params.get_bool("IsReleaseBranch")
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
bundle = params.get("CarPlatformBundle")
bundle_brand = _bundle_field(bundle, "brand")
bundle_platform = _bundle_field(bundle, "platform")
# Bundle-first brand resolution; CP is fallback only.
if bundle_brand:
caps["brand"] = bundle_brand
# CarParams-derived capabilities
CP = None
CP_bytes = params.get("CarParamsPersistent")
if CP_bytes is not None:
try:
CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
caps["alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
if CP.alphaLongitudinalAvailable:
caps["has_longitudinal_control"] = params.get_bool("AlphaLongitudinalEnabled")
else:
caps["has_longitudinal_control"] = bool(CP.openpilotLongitudinalControl)
# CP.steerControlType is the physical control mode (angle / torque).
# CP.lateralTuning.which() returns the tuning class (pid / torque / indi)
# which is a separate concept and is not interchangeable.
caps["steer_control_type"] = str(CP.steerControlType)
caps["torque_allowed"] = CP.steerControlType != car.CarParams.SteerControlType.angle
if not caps["brand"] and CP.brand:
caps["brand"] = str(CP.brand)
caps["pcm_cruise"] = bool(CP.pcmCruise)
caps["enable_bsm"] = bool(CP.enableBsm)
# Generic SnG fallback. Brand-specific opaque flags below override.
caps["has_stop_and_go"] = bool(CP.openpilotLongitudinalControl)
except Exception:
CP = None
cloudlog.exception("capabilities: failed to deserialize CarParamsPersistent")
# CarParamsSP-derived capabilities
CP_SP_bytes = params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
try:
CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
caps["icbm_available"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable)
caps["has_icbm"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable) and params.get_bool("IntelligentCruiseButtonManagement")
caps["tesla_has_vehicle_bus"] = bool(CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
except Exception:
cloudlog.exception("capabilities: failed to deserialize CarParamsSPPersistent")
_resolve_brand_capabilities(caps, bundle_platform, CP)
return caps
def generate_capabilities_json(params: Params | None = None) -> str:
"""Generate SettingsCapabilities as a JSON string."""
return json.dumps(generate_capabilities(params), separators=(",", ":"))

View File

@@ -1,586 +0,0 @@
# sunnylink Settings UI Guide
> One YAML file per page. Edit, run the compiler, commit. The sunnylink frontend updates automatically.
## What you edit (and what's generated)
| File | What | When to edit |
|------|------|-------------|
| `settings_ui_src/pages/<page>.yaml` | One YAML per page (panel). Contains panel metadata + sections + items + sub_panels inline. | Adding/changing/removing a setting. |
| `settings_ui_src/pages/vehicle.yaml` | Per-brand settings page (`kind: vehicle`). Each brand is a section. | Adding/changing a vehicle-specific setting. |
| `settings_ui_src/_macros.yaml` | Named rule fragments referenced via `{$ref: "#/macros/<name>"}`. | Adding a reusable rule (e.g. a new platform gate). |
| **`settings_ui.json`** | **Generated from src tree by `compile_settings_ui.py`. Do not edit by hand.** | Never. Compiler emits it; frontend reads it. |
Pages today: `steering, cruise, display, visuals, toggles, device, software, developer, models, vehicle` (10).
Run `python sunnypilot/sunnylink/tools/compile_settings_ui.py` after edits. Add `--check` in CI to fail on out-of-sync `settings_ui.json`.
Display metadata (titles, descriptions, options, min/max/step/unit) is inline on each item. There is no separate metadata file.
## Page file shape
A page YAML contains the whole panel: metadata at the top, then `sections`. Each section has its own `items` and (optionally) `sub_panels`. Sub-panels are nested inside the section they belong to. Items appear in the order written in the file.
```yaml
# yaml-language-server: $schema=../_schemas/page.schema.json
id: steering
label: Steering
icon: steering_wheel
order: 1
remote_configurable: true
description: Lateral control, lane changes, and steering behavior
sections:
- id: mads
title: Modular Assistive Driving System (MADS)
items:
- key: Mads
widget: toggle
title: Enable Modular Assistive Driving System (MADS)
description: |
Enable the beloved MADS feature. Disable toggle to revert back
to stock sunnypilot engagement/disengagement.
enablement:
- {$ref: "#/macros/offroad"}
sub_panels:
- id: mads_settings
label: MADS Settings
trigger_key: Mads
trigger_condition: {type: param, key: Mads, equals: true}
items:
- key: MadsMainCruiseAllowed
widget: toggle
title: Toggle with Main Cruise
description: |
Note: For vehicles without LFA/LKAS button, disabling this will
prevent lateral control engagement.
enablement:
- {$ref: "#/macros/offroad"}
- {$ref: "#/macros/mads_full_platforms"}
```
The vehicle page has the same shape but declares `kind: vehicle`; each section's `id` becomes a brand key under `vehicle_settings` in the compiled JSON.
## Macros (named rule fragments)
`_macros.yaml` declares reusable rule lists. Reference them from any rules array via `{$ref: "#/macros/<name>"}`.
```yaml
macros:
offroad: [{type: offroad_only}]
longitudinal: [{type: capability, field: has_longitudinal_control, equals: true}]
mads_full_platforms:
- type: not
condition:
type: any
conditions:
- {type: capability, field: brand, equals: rivian}
- type: all
conditions:
- {type: capability, field: brand, equals: tesla}
- type: not
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
```
In an item:
```yaml
enablement:
- {$ref: "#/macros/offroad"}
- {$ref: "#/macros/mads_full_platforms"}
```
The compiler splices a list-context `$ref` into its parent list. Macros may reference other macros up to depth 3; cycles are an error.
## Compiler workflow
```
1. common/params_keys.h — add/remove the C++ param key
2. params_metadata.json — automated via update_params_metadata.py
3. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
4. python sunnypilot/sunnylink/tools/compile_settings_ui.py
5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
7. commit
```
CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`.
## Compiled output reference (schema contract)
The tables below describe the **compiled** `settings_ui.json` schema — what the frontend consumes at runtime. JSON snippets show the wire shape; in the src tree you author YAML that compiles to the same shape. Use these as a contract reference for valid fields, their meanings, and rule types.
## Quick reference: widget types
| Widget | Use for | Fields needed |
|--------|---------|---------------|
| `toggle` | On/off boolean | `title` |
| `multiple_button` | 2-4 discrete options | `title` + `options` array |
| `option` | Numeric range or dropdown | `title` + `min/max/step` or `options` |
| `info` | Read-only display | `title` |
## Quick reference: item fields
| Field | Required | Description |
|-------|----------|-------------|
| `key` | Yes | Param key name (must exist in `params_keys.h`) |
| `widget` | Yes | `toggle`, `option`, `multiple_button`, `button`, `info` |
| `title` | Yes | Display name shown to the user |
| `description` | No | Inline explanatory text below the title. May be empty when only `details` is used. |
| `details` | No | Extended help text shown in a modal when the user taps an "i" button on the row. Independent of `description`: either, both, or neither may be present. |
| `options` | For selectors | Array of `{"value": 0, "label": "Off"}` objects (see per-option enablement below) |
| `min`, `max`, `step` | For sliders | Numeric range constraints |
| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (resolved by IsMetric) |
| `visibility` | No | Rules for show/hide. Settings are never hidden, always dimmed with UNAVAILABLE badge when rules fail |
| `enablement` | No | Rules for enabled/disabled (all must pass). Dimmed with badge when rules fail |
| `blocked` | No | `true` for device-only settings that cannot be modified remotely. Frontend shows as read-only |
| `title_param_suffix` | No | Dynamic title suffix. Example: `{"param": "IsMetric", "values": {"0": "mph", "1": "km/h"}}` |
| `sub_items` | No | Nested child items |
| `needs_onroad_cycle` | No | `true` if changing this param triggers a system restart. Frontend shows a "Restart" badge. See [REFERENCE.md - Remote Onroad Cycle](REFERENCE.md#remote-onroad-cycle) |
## Quick reference: rule types
| Rule | Example | Use for |
|------|---------|---------|
| `offroad_only` | `{"type": "offroad_only"}` | Grey out while driving |
| `not_engaged` | `{"type": "not_engaged"}` | Grey out only while engaged (started + selfdrive/MADS active) |
| `capability` | `{"type": "capability", "field": "has_longitudinal_control", "equals": true}` | Car-dependent visibility |
| `param` | `{"type": "param", "key": "Mads", "equals": true}` | Show/enable based on another setting |
| `param_compare` | `{"type": "param_compare", "key": "SpeedLimitMode", "op": ">", "value": 0}` | Numeric comparison |
| `not` | `{"type": "not", "condition": {...}}` | Negate a rule |
| `any` | `{"type": "any", "conditions": [...]}` | OR logic |
| `all` | `{"type": "all", "conditions": [...]}` | AND logic (for nesting inside `any`/`not`) |
| `$ref` | `{"$ref": "#/macros/offroad"}` | Reference a named rule fragment in `_macros.yaml` |
**Visibility design**: Settings are always visible. When visibility rules fail, the setting is dimmed with an UNAVAILABLE badge, so users know it exists but is not applicable.
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `enable_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
---
## How to
### Pick a writability rule (offroad / not_engaged / param-based)
| Use this | When | Why |
|---|---|---|
| `offroad_only` | Param can only be safely changed when the car is parked. Most user-facing toggles. | Strictest. Frontend shows "device is driving" badge and disables the row. |
| `not_engaged` | Param can be changed while the car is started but only when sunnypilot/MADS is **not** actively driving. | Less strict than offroad. Matches Raylib `engaged = started AND (selfdriveState.enabled OR mads.enabled)`. Use for items the device must apply mid-drive (e.g. test maneuvers, longitudinal stock-vs-OP toggle). |
| `param`-based | Behavior depends on another setting's value (parent toggle, mode selector, etc.). | Composes with `not`/`any`/`all` for arbitrary logic. |
| `capability`-based | Behavior depends on the connected car or device (brand, longitudinal, hardware). | Resolved on the device from `CarParams` / hardware. See [`capabilities.py`](../capabilities.py) for the full field list. |
| (no rule) | Param is always writable, no gating. | Rare. Prefer at least `offroad_only` unless the param is genuinely safe to flip mid-drive. |
Default for new toggles: `enablement: [{$ref: "#/macros/offroad"}]`. Drop down to `not_engaged` only if you've confirmed mid-drive write is safe in the controls/UI code path.
### Use `details` for safety notes / extended help
Inline `description` shows under the title. For longer caveats, safety notes, or "learn more" content, use `details` — the frontend renders an info button that opens a modal. Either field may be present alone or both together.
```yaml
- key: AutoLaneChangeTimer
widget: option
title: Auto Lane Change by Blinker
description: |-
Set a timer to delay the auto lane change operation when the blinker is used.
No nudge on the steering wheel is required to auto lane change if a timer is set.
Default is Nudge.
details: |-
Please use caution when using this feature. Only use the blinker when traffic
and road conditions permit.
options: [...]
```
For an item that is intentionally minimal inline (no inline body, only the modal):
```yaml
- key: SomeAdvancedToggle
widget: toggle
title: Some Advanced Feature
details: |-
Long-form rationale, caveats, links, etc. — kept entirely behind the info button.
```
### Add a toggle
1. Register in `common/params_keys.h`:
```cpp
{"MyToggle", {PERSISTENT | BACKUP, BOOL}},
```
2. Open `settings_ui_src/pages/<page>.yaml`. Add the item to the right section:
```yaml
- key: MyToggle
widget: toggle
title: My Feature
description: What this feature does.
enablement:
- {$ref: "#/macros/offroad"}
```
If changing the param requires an onroad cycle to take effect, add `needs_onroad_cycle: true`.
3. Compile + validate + test:
```
python sunnypilot/sunnylink/tools/compile_settings_ui.py
python sunnypilot/sunnylink/tools/validate_settings_ui.py
uv run python -m pytest sunnypilot/sunnylink/tests/
```
### Add a multi-button option
```yaml
- key: MySelector
widget: multiple_button
title: Mode
options:
- {value: 0, label: Off}
- {value: 1, label: On}
- {value: 2, label: Auto}
```
### Add a slider or range
```yaml
- key: MyRange
widget: option
title: Follow Distance
description: Time gap to lead vehicle.
min: 0.5
max: 3.0
step: 0.1
unit: seconds
```
### Add a slider with metric/imperial units
```yaml
- key: MinSpeed
widget: option
title: Minimum Speed
min: 0
max: 100
step: 5
unit: {metric: km/h, imperial: mph}
```
Frontend resolves the unit string based on the device's `IsMetric` param. Static units (e.g. `seconds`, `m/s²`) stay plain strings.
### Add a dynamic title suffix
```yaml
- key: FollowDistance
widget: option
title: Follow Distance
title_param_suffix:
param: IsMetric
values: {'0': mph, '1': km/h}
min: 0.5
max: 3.0
step: 0.1
```
Renders as "Follow Distance: mph" / "Follow Distance: km/h".
### Add a device-only read-only setting
```yaml
- key: OnroadCyclePendingRemote
widget: info
title: Pending Remote Cycle
blocked: true
```
Frontend treats `blocked: true` items as read-only.
### Add a dropdown option
```yaml
- key: MyDropdown
widget: option
title: Recording Quality
options:
- {value: 0, label: Low (720p)}
- {value: 1, label: Medium (1080p)}
- {value: 2, label: High (4K)}
```
### Per-option enablement rules
```yaml
- key: MadsSteeringMode
widget: multiple_button
title: Steering Mode on Brake Pedal
options:
- value: 0
label: Remain Active
enablement:
- {$ref: "#/macros/mads_full_platforms"}
- value: 1
label: Pause
enablement:
- {$ref: "#/macros/mads_full_platforms"}
- value: 2
label: Disengage
enablement:
- {$ref: "#/macros/offroad"}
```
When an option's enablement fails, that option is grayed out but still visible.
### Show only when another setting is on
```yaml
- key: ChildSetting
widget: toggle
title: Child Feature
visibility:
- {type: param, key: ParentToggle, equals: true}
```
(With the "dim instead of hide" design, this setting is dimmed, not hidden, when the rule fails.)
### Show only for specific brands
```yaml
- key: LongFeature
widget: toggle
title: Longitudinal Feature
visibility:
- {$ref: "#/macros/longitudinal"}
```
### Combine multiple conditions
The `enablement` array is implicit-AND: every entry must pass. Use `any` for OR, `all` for nested AND, `not` for negation. Wrap repeated combinations in a macro so future you doesn't re-derive the logic.
**AND across two params** (writable only when both Mads is on AND ICBM is enabled):
```yaml
enablement:
- {type: param, key: Mads, equals: true}
- {type: param, key: IntelligentCruiseButtonManagement, equals: true}
```
**OR across two params** (writable when either is on):
```yaml
enablement:
- type: any
conditions:
- {type: param, key: ExperimentalMode, equals: true}
- {type: param, key: DynamicExperimentalControl, equals: true}
```
**Mixed: capability AND param** (only on longitudinal cars when ShowAdvancedControls is on):
```yaml
enablement:
- {$ref: "#/macros/longitudinal"}
- {$ref: "#/macros/advanced_only"}
```
**Three-way: offroad AND torque-allowed AND not-NNLC** (real example: `EnforceTorqueControl`):
```yaml
enablement:
- {$ref: "#/macros/offroad"}
- {type: capability, field: torque_allowed, equals: true}
- {type: param, key: NeuralNetworkLateralControl, equals: false}
```
**Negation across multiple platforms** (everything except Rivian + Tesla-no-bus):
```yaml
enablement:
- {$ref: "#/macros/offroad"}
- {$ref: "#/macros/mads_full_platforms"} # macro encapsulates the not(any(rivian, all(tesla, not(bus)))) logic
```
If the same multi-condition block appears in 2+ items, **promote it to a macro** in `_macros.yaml`. Re-run `python sunnypilot/sunnylink/tools/apply_macros.py` to substitute existing inlined matches automatically.
### Mutual exclusion
```yaml
- key: FeatureAlpha
widget: toggle
title: Feature Alpha
enablement:
- {type: param, key: FeatureBeta, equals: false}
- key: FeatureBeta
widget: toggle
title: Feature Beta
enablement:
- {type: param, key: FeatureAlpha, equals: false}
```
### Add a section
In the page YAML, add an entry to the `sections` list:
```yaml
sections:
- id: my_section
title: My Section
description: Optional subtitle
enablement:
- {$ref: "#/macros/longitudinal"}
items:
- {key: ..., widget: toggle, title: ...}
```
Sections support `visibility`, `enablement`, and `attestation_required`. When section-level rules fail, all items within are dimmed.
### Add a sub-panel
Sub-panels nest inside the section they belong to:
```yaml
sections:
- id: parent_section
title: Parent
items: [...]
sub_panels:
- id: my_sub
label: Advanced Settings
trigger_key: ParentParam
trigger_condition: {type: param, key: ParentParam, equals: true}
items:
- {key: ..., widget: toggle, title: ...}
```
### Add vehicle-brand settings
Edit `pages/vehicle.yaml`. Each section is a brand:
```yaml
id: vehicle
kind: vehicle
sections:
- id: rivian
title: Rivian Settings
description: ''
items:
- key: RivianFeature
widget: toggle
title: Rivian One Pedal
enablement:
- {$ref: "#/macros/offroad"}
```
`kind: vehicle` tells the compiler to emit this page as `vehicle_settings.<brand>` in the wire JSON.
### Add a feature with toggles, sub-panel, and macro
Example: "Smart Wipers" with a master toggle, intensity selector, and sub-panel for advanced tuning, gated to torque-steering Hyundais on offroad.
1. **Param keys** — register all 4 in `common/params_keys.h`.
2. **Decide on a macro** — if "torque Hyundai" gating is reused, add to `_macros.yaml`:
```yaml
torque_hyundai:
- {$ref: "#/macros/offroad"}
- {type: capability, field: brand, equals: hyundai}
- {type: capability, field: torque_allowed, equals: true}
```
3. **Edit the relevant page** — `pages/visuals.yaml` (or wherever the feature lives). Add a new section + sub_panel:
```yaml
sections:
- id: smart_wipers
title: Smart Wipers
description: Camera-driven wiper control (Hyundai/Kia, torque only)
items:
- key: SmartWipersEnabled
widget: toggle
title: Enable Smart Wipers
enablement:
- {$ref: "#/macros/torque_hyundai"}
- key: SmartWipersIntensity
widget: multiple_button
title: Sensitivity
options:
- {value: 0, label: Low}
- {value: 1, label: Medium}
- {value: 2, label: High}
visibility:
- {type: param, key: SmartWipersEnabled, equals: true}
enablement:
- {$ref: "#/macros/torque_hyundai"}
sub_panels:
- id: smart_wipers_tuning
label: Smart Wipers Tuning
trigger_key: SmartWipersEnabled
trigger_condition: {type: param, key: SmartWipersEnabled, equals: true}
items:
- key: SmartWipersHysteresis
widget: option
title: Hysteresis (frames)
min: 1
max: 30
step: 1
enablement:
- {$ref: "#/macros/offroad"}
- {$ref: "#/macros/advanced_only"}
```
4. **Compile / validate / test**:
```
python sunnypilot/sunnylink/tools/compile_settings_ui.py
python sunnypilot/sunnylink/tools/validate_settings_ui.py
uv run python -m pytest sunnypilot/sunnylink/tests/
```
`apply_macros.py` is automatic for newly-added items only if you wrote the rule list inline; for greenfield items, you'd write `$ref` directly.
### Change a toggle's behavior
1. Find the item in `pages/<page>.yaml`.
2. Edit `visibility`/`enablement`/`options[].enablement` directly. Use macros where possible.
3. **Add a regression test** in `sunnypilot/sunnylink/tests/test_settings_changes.py` that asserts the new gate exists. Use existing tests (e.g. `TestMadsBrandGates`, `TestNotEngagedReplacement`) as templates: lookup item by key, assert `_references_capability_field(rules, "...")` or `_flatten_rule_types(rules)` contains/excludes a type. This freezes the new behavior so a future edit won't silently revert it.
4. Compile + run the full suite. Per-bug test should pass; structural tests should remain green.
### Change a widget type or options
Editing `widget:` from `toggle` to `multiple_button` is a frontend behavior change. Whenever you change widget shape:
- The param's underlying type (bool / int / string) must match what the new widget writes. `toggle` writes bool; `multiple_button`/`option` write int/string. Update `params_keys.h` if the type changes.
- Add an `options:` list when switching to `multiple_button` or `option`.
- Old values stored on devices may not be valid for the new widget. Consider a migration in `sunnypilot/system/updated/` if users have stale values.
### Deprecate or remove a setting
1. Remove the item from `pages/<page>.yaml`.
2. Remove the param key from `common/params_keys.h` **only after** confirming nothing in `selfdrive/`, `sunnypilot/`, or any controls code reads it.
3. If the param has been on user devices, drop it via a migration (see `sunnypilot/system/updated/`) so stale values don't linger.
4. Compile + validate + test. The validator's "no duplicate keys" + structural checks will fail if anything still references the removed key.
### Move a setting to another page
Cut the item block from one page YAML, paste into the target page's section. Compile + validate. The "no duplicate keys" check catches forgotten copies.
### Change display text
Edit `title:` or `description:` in the page YAML and recompile to regenerate `settings_ui.json`.
### Reorder sections, sub-panels, and items
Reorder them within their parent list in the YAML. The compiler preserves authored order — no `order:` field required at the section/sub_panel/item level (panel-level `order:` controls which page comes first in the side nav).
---
### Capability labels and tooltips
The schema response includes `capability_labels`, which map capability field names to descriptions. The frontend uses these to show contextual tooltips when a capability rule prevents a setting from being used.
The device defines these labels in `capabilities.py:CAPABILITY_LABELS`. Examples:
- `has_longitudinal_control` → "sunnypilot longitudinal control"
- `torque_allowed` → "torque steering (not available for angle steering vehicles)"
- `brand` → "Vehicle brand"
### Centralized param enforcement
The device-side UI enforces capability constraints in `selfdrive/ui/sunnypilot/ui_state.py:_enforce_constraints()`, which removes incompatible params based on car capabilities. This is the single source of truth for such constraints.
Settings layouts should not duplicate these params.remove() calls. Instead, rely on schema rules and centralized enforcement to prevent duplicate logic and ensure consistency.
Example constraints in `_enforce_constraints()`:
- Angle steering cars: remove `EnforceTorqueControl` and `NeuralNetworkLateralControl`
- No CarParams: remove all car-dependent params
- No longitudinal: remove `ExperimentalMode`
- No ICBM: remove `IntelligentCruiseButtonManagement`

View File

@@ -1071,10 +1071,6 @@
"title": "Panda Som Reset Triggered",
"description": ""
},
"ParamsVersion": {
"title": "Params Version",
"description": ""
},
"PlanplusControl": {
"title": "Plan Plus Controls",
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong",
@@ -1114,10 +1110,6 @@
"title": "Record Front Lock",
"description": ""
},
"RoadEdgeLaneChangeEnabled": {
"title": "Block Lane Change: Road Edge Detection",
"description": ""
},
"RoadName": {
"title": "Road Name",
"description": ""

File diff suppressed because it is too large Load Diff

View File

@@ -1,516 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sunnypilot.com/schemas/settings_ui.schema.json",
"title": "sunnypilot Settings UI Schema",
"description": "Defines the structure of the sunnypilot settings UI panels, items, rules, and vehicle-specific settings.",
"type": "object",
"required": ["schema_version", "panels", "vehicle_settings"],
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference for editor support."
},
"schema_version": {
"type": "string",
"description": "Version of the settings UI schema format.",
"examples": ["1.0"]
},
"panels": {
"type": "array",
"description": "Top-level settings panels displayed in the UI.",
"items": {
"$ref": "#/$defs/Panel"
}
},
"vehicle_settings": {
"type": "object",
"description": "Brand-keyed vehicle-specific settings. Each key is a car brand (e.g. 'hyundai', 'toyota').",
"additionalProperties": {
"$ref": "#/$defs/VehicleBrandSettings"
}
}
},
"$defs": {
"Panel": {
"type": "object",
"description": "A top-level settings panel (tab) in the UI.",
"required": ["id", "label", "icon", "order"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this panel."
},
"label": {
"type": "string",
"description": "Display label shown in the UI."
},
"icon": {
"type": "string",
"description": "Icon identifier for this panel."
},
"order": {
"type": "integer",
"description": "Sort order for panel display.",
"minimum": 0
},
"description": {
"type": "string",
"description": "Optional description shown below the panel label."
},
"remote_configurable": {
"type": "boolean",
"description": "Whether this panel's settings can be changed remotely via sunnylink.",
"default": false
},
"sections": {
"type": "array",
"description": "Grouped sections within this panel.",
"items": {
"$ref": "#/$defs/PanelSection"
}
},
"items": {
"type": "array",
"description": "Settings items directly in this panel (no section grouping).",
"items": {
"$ref": "#/$defs/SchemaItem"
}
},
"sub_panels": {
"type": "array",
"description": "Nested sub-panels triggered by a setting.",
"items": {
"$ref": "#/$defs/SubPanel"
}
}
}
},
"PanelSection": {
"type": "object",
"description": "A grouped section within a panel.",
"required": ["id", "title"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this section."
},
"title": {
"type": "string",
"description": "Display title for this section."
},
"description": {
"type": "string",
"description": "Optional description shown below the section title."
},
"order": {
"type": "integer",
"description": "Sort order within the parent panel.",
"minimum": 0
},
"visibility": {
"type": "array",
"description": "Rules that determine whether this section is visible. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"enablement": {
"type": "array",
"description": "Rules that determine whether items in this section are enabled. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"attestation_required": {
"type": "boolean",
"description": "When true, the UI must show an attestation modal before any write to items in this section.",
"default": false
},
"items": {
"type": "array",
"description": "Settings items within this section.",
"items": {
"$ref": "#/$defs/SchemaItem"
}
},
"sub_panels": {
"type": "array",
"description": "Nested sub-panels within this section.",
"items": {
"$ref": "#/$defs/SubPanel"
}
}
}
},
"VehicleBrandSettings": {
"type": "object",
"description": "Brand-specific settings group inside vehicle_settings.",
"required": ["items"],
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
"description": "Display title for this brand's settings group."
},
"description": {
"type": "string",
"description": "Optional description shown below the brand title."
},
"items": {
"type": "array",
"description": "Settings items for this brand.",
"items": {
"$ref": "#/$defs/SchemaItem"
}
}
}
},
"SchemaItem": {
"type": "object",
"description": "A single settings item (toggle, option selector, button group, etc.).",
"required": ["key", "widget"],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
"description": "The param key this item reads/writes."
},
"widget": {
"type": "string",
"description": "The UI widget type to render.",
"enum": ["toggle", "option", "multiple_button", "button", "info"]
},
"title": {
"type": "string",
"description": "Override display title (defaults to metadata lookup by key)."
},
"description": {
"type": "string",
"description": "Override description text. Rendered inline below the title. May be empty when only `details` is used."
},
"details": {
"type": "string",
"description": "Extended help text shown in a popover/modal when the user taps an info ('i') button on the row. Independent of `description`: either, both, or neither may be present."
},
"options": {
"type": "array",
"description": "Available options for 'option' or 'multiple_button' widgets.",
"items": {
"$ref": "#/$defs/SchemaOption"
}
},
"min": {
"type": "number",
"description": "Minimum value for numeric option widgets."
},
"max": {
"type": "number",
"description": "Maximum value for numeric option widgets."
},
"step": {
"type": "number",
"description": "Step increment for numeric option widgets."
},
"unit": {
"oneOf": [
{
"type": "string",
"description": "Static unit label (e.g. 'seconds', 'm/s²')."
},
{
"type": "object",
"description": "Dynamic unit that changes based on IsMetric param.",
"required": ["metric", "imperial"],
"additionalProperties": false,
"properties": {
"metric": {
"type": "string",
"description": "Unit label when IsMetric is true (e.g. 'km/h')."
},
"imperial": {
"type": "string",
"description": "Unit label when IsMetric is false (e.g. 'mph')."
}
}
}
],
"description": "Unit label for numeric values. Use a string for static units or an object with metric/imperial variants for units that depend on the IsMetric param."
},
"value_map": {
"type": "object",
"description": "Maps stored values to display labels.",
"additionalProperties": {
"type": "string"
}
},
"visibility": {
"type": "array",
"description": "Rules that determine whether this item is visible. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"enablement": {
"type": "array",
"description": "Rules that determine whether this item is enabled/interactive. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"sub_items": {
"type": "array",
"description": "Child items nested under this item (e.g. options revealed by a toggle).",
"items": {
"$ref": "#/$defs/SchemaItem"
}
},
"action": {
"type": "string",
"description": "Action identifier for button widgets."
},
"title_param_suffix": {
"type": "object",
"description": "Renders an extra suffix in the item title chosen by the value of another param.",
"required": ["param", "values"],
"additionalProperties": false,
"properties": {
"param": {
"type": "string",
"description": "Param key whose value selects the suffix label."
},
"values": {
"type": "object",
"description": "Map from stringified param value to suffix label.",
"additionalProperties": {
"type": "string"
}
}
}
},
"needs_onroad_cycle": {
"type": "boolean",
"description": "When true, the device must cycle onroad/offroad for the new value to take effect.",
"default": false
},
"blocked": {
"type": "boolean",
"description": "When true, this item is treated as DEVICE_ONLY and the dashboard must not write it remotely.",
"default": false
},
"requires_attestation": {
"type": "boolean",
"description": "When true, writes to this item require an explicit per-write confirmation modal.",
"default": false
}
}
},
"SubPanel": {
"type": "object",
"description": "A nested panel that opens when triggered by a parent item.",
"required": ["id", "label", "trigger_key"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this sub-panel."
},
"label": {
"type": "string",
"description": "Display label for the sub-panel header."
},
"trigger_key": {
"type": "string",
"description": "The param key that triggers opening this sub-panel."
},
"trigger_condition": {
"$ref": "#/$defs/Rule",
"description": "Optional rule that must evaluate to true for the sub-panel trigger to be active."
},
"items": {
"type": "array",
"description": "Settings items within this sub-panel.",
"items": {
"$ref": "#/$defs/SchemaItem"
}
}
}
},
"SchemaOption": {
"type": "object",
"description": "A selectable option for option/multiple_button widgets.",
"required": ["value", "label"],
"additionalProperties": false,
"properties": {
"value": {
"oneOf": [
{ "type": "number" },
{ "type": "string" }
],
"description": "The stored value when this option is selected."
},
"label": {
"type": "string",
"description": "The display label for this option."
},
"enablement": {
"type": "array",
"description": "Rules that determine whether this option is selectable. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
}
}
},
"Rule": {
"description": "A visibility or enablement rule. Discriminated union on the 'type' field.",
"oneOf": [
{ "$ref": "#/$defs/RuleOffroadOnly" },
{ "$ref": "#/$defs/RuleNotEngaged" },
{ "$ref": "#/$defs/RuleCapability" },
{ "$ref": "#/$defs/RuleParam" },
{ "$ref": "#/$defs/RuleParamCompare" },
{ "$ref": "#/$defs/RuleNot" },
{ "$ref": "#/$defs/RuleAny" },
{ "$ref": "#/$defs/RuleAll" }
]
},
"RuleOffroadOnly": {
"type": "object",
"description": "Rule that passes only when the device is offroad.",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {
"const": "offroad_only"
}
}
},
"RuleNotEngaged": {
"type": "object",
"description": "Rule that passes when the vehicle is not engaged (matches Raylib `engaged = started AND (selfdriveState.enabled OR selfdriveStateSP.mads.enabled)`).",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {
"const": "not_engaged"
}
}
},
"RuleCapability": {
"type": "object",
"description": "Rule that checks a vehicle capability field against an expected value.",
"required": ["type", "field", "equals"],
"additionalProperties": false,
"properties": {
"type": {
"const": "capability"
},
"field": {
"type": "string",
"description": "The capability field name to check."
},
"equals": {
"description": "The expected value to match against."
}
}
},
"RuleParam": {
"type": "object",
"description": "Rule that checks a param value against an expected value.",
"required": ["type", "key", "equals"],
"additionalProperties": false,
"properties": {
"type": {
"const": "param"
},
"key": {
"type": "string",
"description": "The param key to read."
},
"equals": {
"description": "The expected value to match against."
}
}
},
"RuleParamCompare": {
"type": "object",
"description": "Rule that compares a numeric param value using a comparison operator.",
"required": ["type", "key", "op", "value"],
"additionalProperties": false,
"properties": {
"type": {
"const": "param_compare"
},
"key": {
"type": "string",
"description": "The param key to read."
},
"op": {
"type": "string",
"description": "Comparison operator.",
"enum": [">", "<", ">=", "<="]
},
"value": {
"type": "number",
"description": "The numeric value to compare against."
}
}
},
"RuleNot": {
"type": "object",
"description": "Rule that negates a single child condition.",
"required": ["type", "condition"],
"additionalProperties": false,
"properties": {
"type": {
"const": "not"
},
"condition": {
"$ref": "#/$defs/Rule",
"description": "The rule to negate."
}
}
},
"RuleAny": {
"type": "object",
"description": "Rule that passes if ANY of the child conditions pass (logical OR).",
"required": ["type", "conditions"],
"additionalProperties": false,
"properties": {
"type": {
"const": "any"
},
"conditions": {
"type": "array",
"description": "Child rules; at least one must pass.",
"items": {
"$ref": "#/$defs/Rule"
},
"minItems": 1
}
}
},
"RuleAll": {
"type": "object",
"description": "Rule that passes only if ALL child conditions pass (logical AND).",
"required": ["type", "conditions"],
"additionalProperties": false,
"properties": {
"type": {
"const": "all"
},
"conditions": {
"type": "array",
"description": "Child rules; all must pass.",
"items": {
"$ref": "#/$defs/Rule"
},
"minItems": 1
}
}
}
}
}

View File

@@ -1,65 +0,0 @@
# Named rule fragments. Reference from items/sections via {$ref: "#/macros/<name>"}.
# Macros may $ref other macros (max depth 3 — see compile_settings_ui.py). No template logic.
#
# Adding a macro: define here once, then reference everywhere. The compiler
# resolves $refs into the canonical settings_ui.json output the frontend reads.
macros:
# Most-used: only writable when the device is offroad.
offroad:
- {type: offroad_only}
# Writable while not engaged (started, but selfdrive/MADS not active).
not_engaged:
- {type: not_engaged}
# sunnypilot longitudinal control is active.
longitudinal:
- {type: capability, field: has_longitudinal_control, equals: true}
# Longitudinal + ICBM both available.
longitudinal_and_icbm:
- {type: capability, field: has_longitudinal_control, equals: true}
- {type: capability, field: has_icbm, equals: true}
# Item only meaningful when "Show Advanced Controls" is enabled by the user.
advanced_only:
- {type: param, key: ShowAdvancedControls, equals: true}
# Hide on MICI hardware (no analog HUD support yet).
hide_on_mici:
- type: not
condition: {type: capability, field: device_type, equals: mici}
# Mirrors selfdrive/ui/sunnypilot/layouts/.../mads_settings.py:_mads_limited_settings()
# Rivian + Tesla-without-vehicle-bus get the limited MADS UI (3-toggle subset).
# On those platforms these toggles are disabled — full MADS settings are
# only writable on platforms NOT in the limited-set.
mads_full_platforms:
- type: not
condition:
type: any
conditions:
- {type: capability, field: brand, equals: rivian}
- type: all
conditions:
- {type: capability, field: brand, equals: tesla}
- type: not
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
# Inverse of mads_full_platforms: present only on the limited platforms.
# Useful for "show this only on rivian/tesla-no-bus" toggles.
mads_limited_platforms:
- type: any
conditions:
- {type: capability, field: brand, equals: rivian}
- type: all
conditions:
- {type: capability, field: brand, equals: tesla}
- type: not
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
# Hide on sunnypilot release branches (is_release is hardcoded False everywhere; is_sp_release is the active gate).
release_branches_hide:
- type: not
condition: {type: capability, field: is_sp_release, equals: true}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sunnypilot.com/schemas/sdui/macros.schema.json",
"title": "Settings UI macros (named rule fragments)",
"type": "object",
"additionalProperties": false,
"required": ["macros"],
"properties": {
"macros": {
"type": "object",
"description": "Named rule fragments. Each value is either a list of rules (typical) or a single rule object. Reference from items/layout via {$ref: '#/macros/<name>'}.",
"patternProperties": {
"^[A-Za-z_][A-Za-z0-9_]*$": {
"oneOf": [
{"type": "array", "items": {"$ref": "rule.schema.json"}},
{"$ref": "rule.schema.json"}
]
}
},
"additionalProperties": false
}
}
}

View File

@@ -1,113 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sunnypilot.com/schemas/sdui/page.schema.json",
"title": "Settings UI page (panel) YAML",
"description": "Validates pages/<id>.yaml. Each page describes one settings panel (or the vehicle namespace via kind: vehicle).",
"type": "object",
"required": ["id"],
"additionalProperties": false,
"properties": {
"id": {"type": "string"},
"label": {"type": "string"},
"icon": {"type": "string"},
"order": {"type": "integer"},
"remote_configurable": {"type": "boolean"},
"description": {"type": "string"},
"kind": {
"type": "string",
"enum": ["panel", "vehicle"],
"description": "panel (default) or vehicle (compiles to settings_ui.json#vehicle_settings)."
},
"sections": {
"type": "array",
"items": {"$ref": "#/$defs/Section"}
},
"items": {
"type": "array",
"items": {"$ref": "#/$defs/Item"}
},
"sub_panels": {
"type": "array",
"items": {"$ref": "#/$defs/SubPanel"}
}
},
"$defs": {
"Section": {
"type": "object",
"required": ["id", "title"],
"additionalProperties": false,
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"order": {"type": "integer"},
"attestation_required": {"type": "boolean"},
"visibility": {"type": "array", "items": {"$ref": "rule.schema.json"}},
"enablement": {"type": "array", "items": {"$ref": "rule.schema.json"}},
"items": {"type": "array", "items": {"$ref": "#/$defs/Item"}},
"sub_panels": {"type": "array", "items": {"$ref": "#/$defs/SubPanel"}}
}
},
"SubPanel": {
"type": "object",
"required": ["id", "label"],
"additionalProperties": false,
"properties": {
"id": {"type": "string"},
"label": {"type": "string"},
"trigger_key": {"type": ["string", "null"]},
"trigger_condition": {"$ref": "rule.schema.json"},
"items": {"type": "array", "items": {"$ref": "#/$defs/Item"}}
}
},
"Item": {
"type": "object",
"required": ["key", "widget"],
"additionalProperties": false,
"properties": {
"key": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"},
"widget": {"type": "string", "enum": ["toggle", "option", "multiple_button", "button", "info"]},
"title": {"type": "string"},
"description": {"type": "string", "description": "Inline body text under the title. May be omitted when only details is used."},
"details": {"type": "string", "description": "Extended help shown in a modal when the user taps the info (i) button. Independent of description; either, both, or neither may be present."},
"title_param_suffix": {"type": "object"},
"min": {"type": "number"},
"max": {"type": "number"},
"step": {"type": "number"},
"unit": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"additionalProperties": false,
"required": ["metric", "imperial"],
"properties": {
"metric": {"type": "string"},
"imperial": {"type": "string"}
}
}
]
},
"needs_onroad_cycle": {"type": "boolean"},
"requires_attestation": {"type": "boolean"},
"blocked": {"type": "boolean"},
"options": {
"type": "array",
"items": {
"type": "object",
"required": ["value", "label"],
"additionalProperties": false,
"properties": {
"value": {},
"label": {"type": "string"},
"enablement": {"type": "array", "items": {"$ref": "rule.schema.json"}}
}
}
},
"visibility": {"type": "array", "items": {"$ref": "rule.schema.json"}},
"enablement": {"type": "array", "items": {"$ref": "rule.schema.json"}},
"sub_items": {"type": "array", "items": {"$ref": "#/$defs/Item"}}
}
}
}
}

View File

@@ -1,101 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sunnypilot.com/schemas/sdui/rule.schema.json",
"title": "Rule",
"description": "Visibility/enablement rule. Discriminated union on 'type'. Macro reference is also accepted via {$ref: '#/macros/<name>'}.",
"oneOf": [
{"$ref": "#/$defs/MacroRef"},
{"$ref": "#/$defs/RuleOffroadOnly"},
{"$ref": "#/$defs/RuleNotEngaged"},
{"$ref": "#/$defs/RuleCapability"},
{"$ref": "#/$defs/RuleParam"},
{"$ref": "#/$defs/RuleParamCompare"},
{"$ref": "#/$defs/RuleNot"},
{"$ref": "#/$defs/RuleAny"},
{"$ref": "#/$defs/RuleAll"}
],
"$defs": {
"MacroRef": {
"type": "object",
"required": ["$ref"],
"additionalProperties": false,
"properties": {
"$ref": {
"type": "string",
"pattern": "^#/macros/[A-Za-z_][A-Za-z0-9_]*$",
"description": "Reference to a macro defined in _macros.yaml under #/macros/<name>."
}
}
},
"RuleOffroadOnly": {
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {"type": {"const": "offroad_only"}}
},
"RuleNotEngaged": {
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {"type": {"const": "not_engaged"}}
},
"RuleCapability": {
"type": "object",
"required": ["type", "field", "equals"],
"additionalProperties": false,
"properties": {
"type": {"const": "capability"},
"field": {"type": "string"},
"equals": {}
}
},
"RuleParam": {
"type": "object",
"required": ["type", "key", "equals"],
"additionalProperties": false,
"properties": {
"type": {"const": "param"},
"key": {"type": "string"},
"equals": {}
}
},
"RuleParamCompare": {
"type": "object",
"required": ["type", "key", "op", "value"],
"additionalProperties": false,
"properties": {
"type": {"const": "param_compare"},
"key": {"type": "string"},
"op": {"type": "string", "enum": [">", "<", ">=", "<="]},
"value": {"type": "number"}
}
},
"RuleNot": {
"type": "object",
"required": ["type", "condition"],
"additionalProperties": false,
"properties": {
"type": {"const": "not"},
"condition": {"$ref": "#"}
}
},
"RuleAny": {
"type": "object",
"required": ["type", "conditions"],
"additionalProperties": false,
"properties": {
"type": {"const": "any"},
"conditions": {"type": "array", "items": {"$ref": "#"}, "minItems": 1}
}
},
"RuleAll": {
"type": "object",
"required": ["type", "conditions"],
"additionalProperties": false,
"properties": {
"type": {"const": "all"},
"conditions": {"type": "array", "items": {"$ref": "#"}, "minItems": 1}
}
}
}
}

View File

@@ -1,269 +0,0 @@
# Page: cruise
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: cruise
label: Cruise
icon: cruise_control
order: 2
remote_configurable: true
description: Longitudinal control, speed limits, and cruise behavior
sections:
- id: core_cruise_features
title: ''
description: ''
items:
- key: ExperimentalMode
widget: toggle
title: Experimental Mode
enablement:
- $ref: '#/macros/longitudinal'
- key: DynamicExperimentalControl
widget: toggle
title: Dynamic Experimental Control
description: Let the model decide when to use sunnypilot ACC or sunnypilot End to End Longitudinal.
visibility:
- $ref: '#/macros/longitudinal'
enablement:
- $ref: '#/macros/longitudinal'
- key: DisengageOnAccelerator
widget: toggle
title: Disengage Cruise on Accelerator Pedal
description: When enabled, pressing the accelerator pedal will disengage longitudinal control.
- key: LongitudinalPersonality
widget: multiple_button
title: Driving Personality
description: Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive
with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can
cycle through these personalities with your steering wheel distance button.
options:
- value: 0
label: Aggressive
- value: 1
label: Standard
- value: 2
label: Relaxed
enablement:
- $ref: '#/macros/longitudinal'
- key: IntelligentCruiseButtonManagement
widget: toggle
title: Intelligent Cruise Button Management (ICBM) (Alpha)
visibility:
- type: capability
field: icbm_available
equals: true
enablement:
- $ref: '#/macros/offroad'
- type: not
condition:
type: capability
field: has_longitudinal_control
equals: true
- id: custom_acc_increments
title: Custom ACC Speed Intervals
description: ''
enablement:
- $ref: '#/macros/offroad'
- type: any
conditions:
- type: all
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: not
condition:
type: capability
field: pcm_cruise
equals: true
- type: capability
field: has_icbm
equals: true
items:
- key: CustomAccIncrementsEnabled
widget: toggle
title: Enable Custom ACC Speed Intervals
enablement:
- $ref: '#/macros/offroad'
- type: any
conditions:
- type: all
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: not
condition:
type: capability
field: pcm_cruise
equals: true
- type: capability
field: has_icbm
equals: true
sub_panels:
- id: custom_acc_intervals
label: Custom ACC Speed Intervals Settings
trigger_key: CustomAccIncrementsEnabled
trigger_condition:
type: param
key: CustomAccIncrementsEnabled
equals: true
items:
- key: CustomAccShortPressIncrement
widget: option
title: Short Press Increment
min: 1
max: 10
step: 1
enablement:
- type: param
key: CustomAccIncrementsEnabled
equals: true
- key: CustomAccLongPressIncrement
widget: option
title: Long Press Increment
min: 1
max: 10
step: 1
enablement:
- type: param
key: CustomAccIncrementsEnabled
equals: true
- id: speed_limits
title: Speed Limits
description: Speed limit detection and offset behavior
items: []
sub_panels:
- id: speed_limit_settings
label: Speed Limit Settings
trigger_key: SpeedLimitMode
items:
- key: SpeedLimitMode
widget: multiple_button
title: Speed Limit Assist Mode
options:
- value: 0
label: 'Off'
- value: 1
label: Information
- value: 2
label: Warning
- value: 3
label: Assist
enablement:
- type: any
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: capability
field: has_icbm
equals: true
- type: not
condition:
type: capability
field: brand
equals: rivian
- type: not
condition:
type: all
conditions:
- type: capability
field: brand
equals: tesla
- type: capability
field: is_sp_release
equals: true
- key: SpeedLimitPolicy
widget: multiple_button
title: Speed Limit Source
options:
- value: 0
label: Car State Only
- value: 1
label: Map Data Only
- value: 2
label: Car State Priority
- value: 3
label: Map Data Priority
- value: 4
label: Combined
- key: SpeedLimitOffsetType
widget: multiple_button
title: Speed Limit Offset Type
options:
- value: 0
label: 'Off'
- value: 1
label: Fixed
- value: 2
label: Percentage
- key: SpeedLimitValueOffset
widget: option
title: Speed Limit Offset Value
min: -30
max: 30
step: 1
unit:
metric: km/h
imperial: mph
visibility:
- type: param_compare
key: SpeedLimitOffsetType
op: '>'
value: 0
- id: smart_cruise
title: Smart Cruise Control
description: ''
enablement:
- type: any
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: capability
field: has_icbm
equals: true
items:
- key: SmartCruiseControlVision
widget: toggle
title: Vision
description: Use vision path predictions to estimate the appropriate speed to drive through turns ahead.
visibility:
- type: any
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: capability
field: has_icbm
equals: true
enablement:
- type: any
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: capability
field: has_icbm
equals: true
- key: SmartCruiseControlMap
widget: toggle
title: Map
description: Use map data to estimate the appropriate speed to drive through turns ahead.
visibility:
- type: any
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: capability
field: has_icbm
equals: true
enablement:
- type: any
conditions:
- type: capability
field: has_longitudinal_control
equals: true
- type: capability
field: has_icbm
equals: true

View File

@@ -1,137 +0,0 @@
# Page: developer
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: developer
label: Developer
icon: developer
order: 9
remote_configurable: true
description: Debug tools, remote access, and advanced services
sections:
- id: connectivity
title: Connectivity
description: Remote access and debugging interfaces
items:
- key: AdbEnabled
widget: toggle
blocked: true
title: Enable ADB
description: ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma
for more info.
enablement:
- $ref: '#/macros/offroad'
- key: SshEnabled
widget: toggle
blocked: true
title: Enable SSH
- key: JoystickDebugMode
widget: toggle
title: Joystick Debug Mode
enablement:
- $ref: '#/macros/offroad'
- key: AlphaLongitudinalEnabled
widget: toggle
needs_onroad_cycle: true
title: sunnypilot Longitudinal Control (Alpha)
description: 'WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking
(AEB). On this car, sunnypilot defaults to the car''s built-in ACC instead of sunnypilot''s longitudinal control. Enable
this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot
longitudinal control alpha. Changing this setting will restart sunnypilot if the car is powered on.'
visibility:
- type: all
conditions:
- type: capability
field: alpha_long_available
equals: true
- type: not
condition:
type: capability
field: has_icbm
equals: true
enablement:
- $ref: '#/macros/not_engaged'
- key: ShowDebugInfo
widget: toggle
title: UI Debug Mode
- id: test_maneuvers
title: Test Maneuvers
description: 'DANGER: enabling these maneuvers replaces normal driving behavior with deterministic test sequences. Each
toggle requires explicit confirmation per write. Use only in a closed environment.'
visibility:
- type: any
conditions:
- type: capability
field: is_development
equals: true
- type: capability
field: is_sp_release
equals: true
enablement:
- type: any
conditions:
- type: capability
field: is_development
equals: true
- type: param
key: ShowAdvancedControls
equals: true
attestation_required: true
items:
- key: LateralManeuverMode
widget: toggle
title: '[TEST] Lateral Maneuver Mode'
description: Replaces normal lateral control with a deterministic test sequence. NOT for road use.
enablement:
- $ref: '#/macros/not_engaged'
- type: capability
field: torque_allowed
equals: true
- key: LongitudinalManeuverMode
widget: toggle
title: '[TEST] Longitudinal Maneuver Mode'
description: Replaces normal longitudinal control with a deterministic test sequence. NOT for road use.
enablement:
- $ref: '#/macros/not_engaged'
- $ref: '#/macros/longitudinal'
- id: advanced_services
title: Advanced Settings
description: ''
items:
- key: ShowAdvancedControls
widget: toggle
title: Show Advanced Controls
description: Toggle visibility of advanced sunnypilot controls. This only changes the visibility of the toggles; it does
not change the actual enabled/disabled state.
- key: EnableGithubRunner
widget: toggle
title: GitHub Runner Service
description: Enables or disables the GitHub runner service.
visibility:
- $ref: '#/macros/release_branches_hide'
enablement:
- $ref: '#/macros/advanced_only'
- key: EnableCopyparty
widget: toggle
title: copyparty Service
description: copyparty is a very capable file server, you can use it to download your routes, view your logs and even
make some edits on some files from your browser. Requires you to connect to your comma locally via its IP address.
enablement:
- $ref: '#/macros/advanced_only'
- key: QuickBootToggle
widget: toggle
title: Quickboot Mode
visibility:
- type: not
condition:
type: any
conditions:
- type: capability
field: is_sp_release
equals: true
- type: capability
field: is_development
equals: true
enablement:
- type: param
key: DisableUpdates
equals: true
- $ref: '#/macros/advanced_only'

View File

@@ -1,67 +0,0 @@
# Page: device
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: device
label: Device
icon: device
order: 6
remote_configurable: true
description: Device behavior, units, and recording settings
sections:
- id: general
title: General
description: Power, boot, and unit preferences
items:
- key: OffroadMode
widget: toggle
title: Force Offroad Mode
- key: DeviceBootMode
widget: option
title: Wake Up Behavior
description: 'Controls state of the device after boot/sleep. Default: Device will boot/wake-up normally and will be ready
to engage. Offroad: Device will be in Always Offroad mode after boot/wake-up.'
options:
- value: 0
label: Standard
- value: 1
label: Always Offroad
- key: QuietMode
widget: toggle
title: Quiet Mode
- key: OnroadUploads
widget: toggle
title: Onroad Uploads
- key: MaxTimeOffroad
widget: option
title: Max Time Offroad
description: Device will automatically shutdown after set time once the engine is turned off. 30h is the default.
options:
- value: 0
label: Always On
- value: 5
label: 5m
- value: 10
label: 10m
- value: 15
label: 15m
- value: 30
label: 30m
- value: 60
label: 1h
- value: 120
label: 2h
- value: 180
label: 3h
- value: 300
label: 5h
- value: 600
label: 10h
- value: 1440
label: 24h
- value: 1800
label: 30h (Default)
- id: language
title: Language
items:
- key: LanguageSetting
widget: info
title: Language

View File

@@ -1,130 +0,0 @@
# Page: display
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: display
label: Display
icon: display
order: 3
remote_configurable: true
description: Screen brightness, timeout, and interactivity settings
sections:
- id: brightness_timeout
title: Brightness & Timeout
description: Screen dimming and sleep behavior while driving
items:
- key: OnroadScreenOffBrightness
widget: multiple_button
title: Onroad Brightness
options:
- value: 0
label: Auto (Default)
- value: 1
label: Auto (Dark)
- value: 2
label: Screen Off
- value: 3
label: 5 %
- value: 4
label: 10 %
- value: 5
label: 15 %
- value: 6
label: 20 %
- value: 7
label: 25 %
- value: 8
label: 30 %
- value: 9
label: 35 %
- value: 10
label: 40 %
- value: 11
label: 45 %
- value: 12
label: 50 %
- value: 13
label: 55 %
- value: 14
label: 60 %
- value: 15
label: 65 %
- value: 16
label: 70 %
- value: 17
label: 75 %
- value: 18
label: 80 %
- value: 19
label: 85 %
- value: 20
label: 90 %
- value: 21
label: 95 %
- value: 22
label: 100 %
- key: OnroadScreenOffTimer
widget: multiple_button
title: Onroad Brightness Delay
options:
- value: 0
label: Always On
- value: 3
label: 3s
- value: 5
label: 5s
- value: 10
label: 10s
- value: 15
label: 15s
- value: 30
label: 30s
- value: 60
label: 1m
- value: 180
label: 3m
- value: 300
label: 5m
- value: 600
label: 10m
enablement:
- type: not
condition:
type: any
conditions:
- type: param
key: OnroadScreenOffBrightness
equals: 0
- type: param
key: OnroadScreenOffBrightness
equals: 1
- key: InteractivityTimeout
widget: multiple_button
title: Interactivity Timeout
description: Apply a custom timeout for settings UI. This is the time after which settings UI closes automatically if
user is not interacting with the screen.
options:
- value: 0
label: Default
- value: 10
label: 10 s
- value: 20
label: 20 s
- value: 30
label: 30 s
- value: 40
label: 40 s
- value: 50
label: 50 s
- value: 60
label: 1 m
- value: 70
label: 1 m
- value: 80
label: 1 m
- value: 90
label: 1 m
- value: 100
label: 1 m
- value: 110
label: 1 m
- value: 120
label: 2 m

View File

@@ -1,89 +0,0 @@
# Page: models
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: models
label: Models
icon: models
order: 10
remote_configurable: false
description: Driving model behavior and camera calibration
sections:
- id: model_behavior
title: Model Behavior
description: Lane desire and lead-vehicle awareness tuning
items:
- key: LaneTurnDesire
widget: toggle
title: Use Lane Turn Desires
description: If you are driving at 20 mph (32 km/h) or below and have your blinker on, the car will plan a turn in that
direction at the nearest drivable path. This prevents situations (like at red lights) where the car might plan the wrong
turn direction.
- key: LaneTurnValue
widget: option
title: Adjust Lane Turn Speed
description: Set the maximum speed for lane turn desires.
min: 0
max: 20
step: 1
unit:
metric: km/h
imperial: mph
enablement:
- type: param
key: LaneTurnDesire
equals: true
- $ref: '#/macros/advanced_only'
- key: LagdToggle
widget: toggle
title: Live Learning Steer Delay
description: Allow device to learn and adapt car's steering response time
- key: LagdToggleDelay
widget: option
title: Adjust Software Delay
description: Adjust the software delay when Live Learning Steer Delay is toggled off. The default software delay value
is 0.2
min: 0.05
max: 0.5
step: 0.01
enablement:
- type: not
condition:
type: param
key: LagdToggle
equals: true
- $ref: '#/macros/advanced_only'
- id: lateral_control
title: Lateral Control
description: Neural network lateral control for supported models
items:
- key: NeuralNetworkLateralControl
widget: toggle
title: Neural Network Lateral Control (NNLC)
description: Use a neural network for lateral control instead of the default torque controller.
visibility:
- type: not
condition:
type: capability
field: steer_control_type
equals: angle
enablement:
- $ref: '#/macros/offroad'
- type: capability
field: torque_allowed
equals: true
- type: param
key: EnforceTorqueControl
equals: false
- id: camera
title: Camera
description: Camera position and calibration
items:
- key: CameraOffset
widget: option
title: Adjust Camera Offset
description: Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)
min: -0.35
max: 0.35
step: 0.01
unit: meters
enablement:
- $ref: '#/macros/advanced_only'

View File

@@ -1,20 +0,0 @@
# Page: software
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: software
label: Software
icon: software
order: 7
remote_configurable: true
description: Software update preferences
sections:
- id: updates
title: Updates
description: Control automatic software updates
items:
- key: DisableUpdates
widget: toggle
title: Disable Updates
description: When enabled, automatic software updates will be off. This requires a reboot to take effect.
enablement:
- $ref: '#/macros/offroad'
- $ref: '#/macros/advanced_only'

View File

@@ -1,257 +0,0 @@
# Page: steering
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: steering
label: Steering
icon: steering_wheel
order: 1
remote_configurable: true
description: Lateral control, lane changes, and steering behavior
sections:
- id: mads
title: Modular Assistive Driving System (MADS)
description: ''
items:
- key: Mads
widget: toggle
title: Enable Modular Assistive Driving System (MADS)
description: Enable MADS. Disable toggle to revert back to stock sunnypilot engagement/disengagement.
enablement:
- $ref: '#/macros/offroad'
sub_panels:
- id: mads_settings
label: MADS Settings
trigger_key: Mads
trigger_condition:
type: param
key: Mads
equals: true
items:
- key: MadsMainCruiseAllowed
widget: toggle
title: Toggle with Main Cruise
description: 'Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement.'
enablement:
- $ref: '#/macros/offroad'
- $ref: '#/macros/mads_full_platforms'
- key: MadsUnifiedEngagementMode
widget: toggle
title: Unified Engagement Mode (UEM)
description: 'Engage lateral and longitudinal control with cruise control engagement. Note: Once lateral control is
engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off.'
enablement:
- $ref: '#/macros/offroad'
- $ref: '#/macros/mads_full_platforms'
- key: MadsSteeringMode
widget: multiple_button
title: Steering Mode on Brake Pedal
description: Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.
options:
- value: 0
label: Remain Active
enablement:
- $ref: '#/macros/mads_full_platforms'
- value: 1
label: Pause
enablement:
- $ref: '#/macros/mads_full_platforms'
- value: 2
label: Disengage
enablement:
- $ref: '#/macros/offroad'
- id: blinker
title: Blinker Control
description: Lateral pause behavior during turn signals
items:
- key: BlinkerPauseLateralControl
widget: toggle
title: Pause Lateral Control with Blinker
description: Pause lateral control with blinker when traveling below the desired speed selected.
sub_items:
- key: BlinkerMinLateralControlSpeed
widget: option
title: Minimum Speed to Pause Lateral Control
min: 0
max: 255
step: 5
unit:
metric: km/h
imperial: mph
enablement:
- type: param
key: BlinkerPauseLateralControl
equals: true
- key: BlinkerLateralReengageDelay
widget: option
title: Post-Blinker Delay
description: Delay before lateral control resumes after the turn signal ends.
min: 0
max: 10
step: 1
unit: second
enablement:
- type: param
key: BlinkerPauseLateralControl
equals: true
- id: torque
title: Torque Control
description: Steering torque tuning and lateral control method
enablement:
- type: capability
field: torque_allowed
equals: true
items:
- key: EnforceTorqueControl
widget: toggle
title: Enforce Torque Lateral Control
description: Enable this to enforce sunnypilot to steer with Torque lateral control.
visibility:
- type: not
condition:
type: capability
field: steer_control_type
equals: angle
enablement:
- $ref: '#/macros/offroad'
- type: capability
field: torque_allowed
equals: true
- type: param
key: NeuralNetworkLateralControl
equals: false
sub_panels:
- id: torque_settings
label: Torque Settings
trigger_key: EnforceTorqueControl
trigger_condition:
type: param
key: EnforceTorqueControl
equals: true
items:
- key: LiveTorqueParamsToggle
widget: toggle
title: Self-Tune
description: Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default.
enablement:
- $ref: '#/macros/offroad'
- key: LiveTorqueParamsRelaxedToggle
widget: toggle
title: Less Restrict Settings for Self-Tune (Beta)
description: Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values.
enablement:
- $ref: '#/macros/offroad'
- type: param
key: LiveTorqueParamsToggle
equals: true
- key: CustomTorqueParams
widget: toggle
title: Enable Custom Tuning
description: Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below
will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also
be used live when "Manual Real-Time Tuning" toggle is enabled.
enablement:
- $ref: '#/macros/offroad'
- key: TorqueParamsOverrideEnabled
widget: toggle
title: Manual Real-Time Tuning
description: Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune.
Enabling this toggle overrides Self-Tune values.
enablement:
- $ref: '#/macros/offroad'
- type: param
key: CustomTorqueParams
equals: true
- key: TorqueParamsOverrideLatAccelFactor
widget: option
title: Lateral Acceleration Factor
title_param_suffix:
param: TorqueParamsOverrideEnabled
values:
'true': (Real-Time & Offline)
'false': (Offline Only)
min: 0.1
max: 5.0
step: 0.1
unit: m/s²
enablement:
- type: param
key: CustomTorqueParams
equals: true
- type: any
conditions:
- type: param
key: TorqueParamsOverrideEnabled
equals: true
- type: offroad_only
- key: TorqueParamsOverrideFriction
widget: option
title: Friction
title_param_suffix:
param: TorqueParamsOverrideEnabled
values:
'true': (Real-Time & Offline)
'false': (Offline Only)
min: 0.0
max: 1.0
step: 0.01
enablement:
- type: param
key: CustomTorqueParams
equals: true
- type: any
conditions:
- type: param
key: TorqueParamsOverrideEnabled
equals: true
- type: offroad_only
- key: TorqueControlTune
widget: multiple_button
title: Torque Control Tune Version
description: Select the version of Torque Control Tune to use.
options:
- value: ''
label: Default
- value: 1.0
label: v1.0
- value: 0.0
label: v0.0
enablement:
- $ref: '#/macros/offroad'
- id: lane_change
title: Lane Change
description: Automatic lane change timing and behavior
items:
- key: AutoLaneChangeTimer
widget: option
title: Auto Lane Change by Blinker
description: |-
Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge.
details: |-
Please use caution when using this feature. Only use the blinker when traffic and road conditions permit.
options:
- value: -1
label: 'Off'
- value: 0
label: Nudge
- value: 1
label: Nudgeless
- value: 2
label: 0.5 second
- value: 3
label: 1 second
- value: 4
label: 2 seconds
- value: 5
label: 3 seconds
- key: AutoLaneChangeBsmDelay
widget: toggle
title: 'Auto Lane Change: Delay with Blind Spot'
description: Toggle to enable a delay timer for lane changes when blind spot monitoring (BSM) detects a vehicle in your blind
spot.
enablement:
- type: capability
field: enable_bsm
equals: true
- type: param_compare
key: AutoLaneChangeTimer
op: '>'
value: 0

View File

@@ -1,49 +0,0 @@
# Page: toggles
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: toggles
label: Toggles
icon: toggles
order: 5
remote_configurable: true
description: Core openpilot feature toggles
sections:
- id: core_toggles
title: ''
description: ''
items:
- key: OpenpilotEnabledToggle
widget: toggle
needs_onroad_cycle: true
title: Enable sunnypilot
description: Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is
required at all times to use this feature.
enablement:
- $ref: '#/macros/offroad'
- key: IsLdwEnabled
widget: toggle
title: Enable Lane Departure Warnings
description: Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn
signal activated while driving over 31 mph (50 km/h).
- key: AlwaysOnDM
widget: toggle
title: Always-On Driver Monitoring
description: Enable driver monitoring even when sunnypilot is not engaged.
- key: IsMetric
widget: toggle
title: Use Metric System
description: Display speed in km/h instead of mph.
- id: recording
title: Recording
description: Camera and audio recording during drives
items:
- key: RecordFront
widget: toggle
needs_onroad_cycle: true
title: Record and Upload Driver Camera
description: Upload data from the driver facing camera and help improve the driver monitoring algorithm.
- key: RecordAudio
widget: toggle
needs_onroad_cycle: true
title: Record and Upload Microphone Audio
description: Record and store microphone audio while driving. The audio will be included in the dashcam video in comma
connect.

View File

@@ -1,81 +0,0 @@
# Page: vehicle (per-brand settings)
# Compiles to settings_ui.json#vehicle_settings (brand = section id).
id: vehicle
label: Vehicle
icon: vehicle
order: 99
kind: vehicle
sections:
- id: hyundai
title: Hyundai / Kia / Genesis Settings
description: ''
items:
- key: HyundaiLongitudinalTuning
widget: multiple_button
title: Custom Longitudinal Tuning
options:
- value: 0
label: 'Off'
- value: 1
label: Dynamic
- value: 2
label: Predictive
visibility:
- type: capability
field: hyundai_alpha_long_available
equals: true
enablement:
- $ref: '#/macros/offroad'
- $ref: '#/macros/longitudinal'
- id: subaru
title: Subaru Settings
description: ''
items:
- key: SubaruStopAndGo
widget: toggle
title: Stop and Go (Beta)
enablement:
- $ref: '#/macros/offroad'
- type: capability
field: has_stop_and_go
equals: true
- key: SubaruStopAndGoManualParkingBrake
widget: toggle
title: Stop and Go for Manual Parking Brake (Beta)
enablement:
- $ref: '#/macros/offroad'
- type: capability
field: has_stop_and_go
equals: true
- id: tesla
title: Tesla Settings
description: ''
items:
- key: TeslaCoopSteering
widget: toggle
title: Cooperative Steering (Beta)
enablement:
- $ref: '#/macros/offroad'
- id: toyota
title: Toyota / Lexus Settings
description: ''
items:
- key: ToyotaEnforceStockLongitudinal
widget: toggle
needs_onroad_cycle: true
title: Enforce Factory Longitudinal Control
description: sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used.
enablement:
- $ref: '#/macros/not_engaged'
- key: ToyotaStopAndGoHack
widget: toggle
needs_onroad_cycle: true
title: Stop and Go Hack (Alpha)
description: sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. This feature is only
applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.
enablement:
- $ref: '#/macros/not_engaged'
- $ref: '#/macros/longitudinal'
- type: param
key: ToyotaEnforceStockLongitudinal
equals: false

View File

@@ -1,111 +0,0 @@
# Page: visuals
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
id: visuals
label: Visuals
icon: visuals
order: 4
remote_configurable: true
description: HUD overlays, alerts, and on-screen display elements
sections:
- id: hud_elements
title: HUD Elements
description: Overlays shown on the driving screen
items:
- key: BlindSpot
widget: toggle
title: Show Blind Spot Warnings
description: Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has
BSM supported.
- key: TorqueBar
widget: toggle
title: Steering Arc
description: Display steering arc on the driving screen when lateral control is enabled.
- key: ShowTurnSignals
widget: toggle
title: Display Turn Signals
description: When enabled, visual turn indicators are drawn on the HUD.
- key: RoadNameToggle
widget: toggle
title: Display Road Name
description: Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be
downloaded to fetch the road name.
visibility:
- $ref: '#/macros/hide_on_mici'
- key: StandstillTimer
widget: toggle
title: Standstill Timer
description: Show a timer on the HUD when the car is at a standstill.
visibility:
- $ref: '#/macros/hide_on_mici'
- key: RocketFuel
widget: toggle
title: Real-time Acceleration Bar
description: Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration.
This displays what the car is currently doing, not what the planner is requesting.
visibility:
- $ref: '#/macros/hide_on_mici'
- key: ChevronInfo
widget: option
title: Display Metrics Below Chevron
options:
- value: 0
label: 'Off'
- value: 1
label: Distance
- value: 2
label: Speed
- value: 3
label: Time
- value: 4
label: All
visibility:
- $ref: '#/macros/hide_on_mici'
enablement:
- $ref: '#/macros/longitudinal'
- id: developer_ui
title: Developer UI Info
description: Speedometer and debug display options
visibility:
- $ref: '#/macros/hide_on_mici'
items:
- key: DevUIInfo
widget: option
title: Developer UI
description: Display real-time parameters and metrics from various sources.
options:
- value: 0
label: 'Off'
- value: 1
label: Bottom
- value: 2
label: Right
- value: 3
label: Right & Bottom
- key: TrueVEgoUI
widget: toggle
title: 'Speedometer: Always Display True Speed'
description: For applicable vehicles, always display the true vehicle current speed from wheel speed sensors.
- key: HideVEgoUI
widget: toggle
title: 'Speedometer: Hide from Onroad Screen'
description: When enabled, the speedometer on the onroad screen is not displayed.
- id: alerts_extras
title: Alerts & Extras
description: Traffic light alerts and visual flair
items:
- key: GreenLightAlert
widget: toggle
title: Green Traffic Light Alert (Beta)
description: 'A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have
no vehicle in front of you. On-screen visual alert is only available on comma 3X. Note: This chime is only designed
as a notification. It is the driver''s responsibility to observe their environment and make decisions accordingly.'
- key: LeadDepartAlert
widget: toggle
title: Lead Departure Alert (Beta)
description: 'A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving.
On-screen visual alert is only available on comma 3X. Note: This chime is only designed as a notification. It is the
driver''s responsibility to observe their environment and make decisions accordingly.'
- key: RainbowMode
widget: toggle
title: Tesla Rainbow Mode
description: Display a rainbow effect on the path the model wants to take. It does not affect driving in any way.

View File

@@ -1,92 +0,0 @@
"""
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.
Sentinel tests for the capabilities payload contract. PROTOCOL_VERSION is the
wire-protocol version observable by the dashboard; bumping it is a breaking
change and must be intentional. KNOWN_PROTOCOL_VERSIONS pins the set we
explicitly support — when the constant is bumped, this list must be edited in
the same commit so the bump shows up in code review.
"""
from __future__ import annotations
import pytest
from openpilot.sunnypilot.sunnylink.capabilities import (
CAPABILITY_DEFAULTS,
CAPABILITY_FIELDS,
CAPABILITY_LABELS,
PROTOCOL_VERSION,
generate_capabilities,
)
KNOWN_PROTOCOL_VERSIONS = (1,)
LATEST_KNOWN = max(KNOWN_PROTOCOL_VERSIONS)
@pytest.fixture(scope="module")
def caps():
return generate_capabilities()
class TestProtocolVersion:
def test_protocol_version_in_capability_fields(self):
assert "protocol_version" in CAPABILITY_FIELDS
def test_protocol_version_has_label(self):
assert "protocol_version" in CAPABILITY_LABELS
def test_protocol_version_default_is_set(self):
assert CAPABILITY_DEFAULTS.get("protocol_version") == PROTOCOL_VERSION
def test_protocol_version_emitted(self, caps):
assert "protocol_version" in caps
assert isinstance(caps["protocol_version"], int)
assert caps["protocol_version"] >= 1
def test_protocol_version_matches_constant(self, caps):
assert caps["protocol_version"] == PROTOCOL_VERSION
def test_protocol_version_is_known(self):
"""Sentinel against accidental bumps. Edit KNOWN_PROTOCOL_VERSIONS if intentional."""
assert PROTOCOL_VERSION in KNOWN_PROTOCOL_VERSIONS, (
f"PROTOCOL_VERSION={PROTOCOL_VERSION} is not in KNOWN_PROTOCOL_VERSIONS={KNOWN_PROTOCOL_VERSIONS}. " +
"If this bump is intentional, add it to KNOWN_PROTOCOL_VERSIONS."
)
def test_protocol_version_matches_latest_known(self):
assert PROTOCOL_VERSION == LATEST_KNOWN, (
"Test invariant: PROTOCOL_VERSION must equal max(KNOWN_PROTOCOL_VERSIONS)."
)
class TestOpaquePerBrandFlags:
def test_subaru_has_sng_field_present(self):
assert "subaru_has_sng" in CAPABILITY_FIELDS
def test_hyundai_alpha_long_available_field_present(self):
assert "hyundai_alpha_long_available" in CAPABILITY_FIELDS
def test_subaru_has_sng_default_false(self, caps):
assert caps["subaru_has_sng"] is False
def test_hyundai_alpha_long_available_default_false(self, caps):
assert caps["hyundai_alpha_long_available"] is False
class TestCapabilitiesShape:
def test_all_fields_present(self, caps):
for field in CAPABILITY_FIELDS:
assert field in caps, f"capabilities missing {field}"
def test_all_fields_have_labels(self):
for field in CAPABILITY_FIELDS:
assert field in CAPABILITY_LABELS, f"CAPABILITY_LABELS missing {field}"
def test_string_defaults_are_strings(self, caps):
assert isinstance(caps["brand"], str)
assert isinstance(caps["steer_control_type"], str)
assert isinstance(caps["device_type"], str)

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