Compare commits

...

212 Commits

Author SHA1 Message Date
James Vecellio 5563618c73 Function level import for now until i actually want to deal with this 2026-01-24 08:20:44 -08:00
James Vecellio 881d06867d Test full blend 2026-01-23 16:17:15 -08:00
James Vecellio 1067145641 Merge remote-tracking branch 'origin/master' into only-chubbs
# Conflicts:
#	opendbc_repo
#	selfdrive/ui/sunnypilot/ui_state.py
2026-01-23 15:55:04 -08:00
Jason Wen 27a8837422 Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1645) 2026-01-20 22:32:31 -05:00
Jason Wen 53327edb50 Merge branch 'upstream/openpilot/master' into sync-20260111
# Conflicts:
#	common/api.py
#	docs/CARS.md
#	opendbc_repo
#	panda
#	scripts/lint/lint.sh
#	selfdrive/car/car_specific.py
#	selfdrive/car/card.py
#	selfdrive/test/process_replay/ref_commit
#	system/hardware/hardwared.py
#	tinygrad_repo
2026-01-20 07:29:25 -05:00
Adeeb Shihadeh 6c7f3751e7 camerad: calculate buffer sizes with VENUS helpers (#37006)
* Revert "NV12 buffer shape helpers (#36683)"

This reverts commit 13efc421c4.

* camerad: calculate buffer sizes with VENUS helpers

* copy header:

* assert aligned

* python nv12 info

* debug

* handle padding

* use the helper
2026-01-19 17:18:22 -08:00
Mauricio Alvarez Leon c179a3ccb7 CI: enable macos tests (#37005)
enable macos tests
2026-01-19 16:45:45 -08:00
Harald Schäfer 13efc421c4 NV12 buffer shape helpers (#36683)
* Give this a try

* can codex debug?

* simpler

* Revert "simpler"

This reverts commit 572335008c1c719aa985d14bd740253ff94b94a9.

* better

* cleanup

* try again

* tie

* try this

* try this

* do tests fail without this?

* doesn't seem needed

* unused

* don't need duplicate

* passes CI?

* try this

* try this

* try this

* I don't understand this, so back to before

* keep that alignment

* set uv_height

---------

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-01-19 16:27:41 -08:00
Adeeb Shihadeh 10db1edc7f merge common.util and common.utils (#36951)
* common: merge common.util and common.utils

* cleanup

* cleanup
2026-01-19 15:50:00 -08:00
Adeeb Shihadeh 039b85f355 bump opendbc (#37003)
* bump opendbc

* bump

* bump

* bump

* bump bump bump
2026-01-19 15:33:23 -08:00
Harald Schäfer 0b41b42f7b WMI model 🍉 (#36798)
* 1791ea0f-8667-4e0b-be73-084d912f6c4c/100

* eab53871-1f8c-45be-9a98-f6b3dd6a0adc/100

* dd075c9d-0c49-402e-b4f2-9adbe5301c84/100

* e8b5b1b0-2d37-4b62-bd39-21ff0d08ee68/100

* 1aff00c7-06c5-46a6-8a79-7e56f77d81bf/100

* 3547a2cc-1699-4e7d-a2ab-4eb87d0b8684/100

* 849aa9fb-dae6-4604-923e-050883def218/100

* 0e0f6dd2-96dc-4f34-a7cd-63bccc2f5616/100

* 887f923b-7e79-43c6-8f1f-053e1490f859/100

* 1fa82260-1171-4db5-9968-d34ce2e14694/100

* Revert "1fa82260-1171-4db5-9968-d34ce2e14694/100"

This reverts commit 855f5e4ddefd69a20cc4e9da004eb53f3e00d950.

* a27b3122-733e-4a65-938b-acfebebbe5e8/100

---------

Co-authored-by: Yassine Yousfi <yyousfi1@binghamton.edu>
2026-01-19 11:48:06 -08:00
commaci-public a46ff01cab [bot] Update Python packages (#36966)
* Update Python packages

* ty fixes

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-01-19 11:39:21 -08:00
Nayan e7b6e62b82 [TIZI/TICI] ui: expose Interactivity Timeout option (#1497)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* Option Control

* Need this

* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* better padding

* this

* listitem -> listitemsp

* Revert "add ui_update callback"

This reverts commit 4da32cc009.

* add show_description method

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

* simplify

* I. SAID. SIMPLIFY.

* AAARGGGGGG.....

* init

* option control value fix

* add all controls

* hide all controls

* lint

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* optimizations

* Removed hide for now

* refresh controls

* ugh

* global brightness

* initialize

* inline everything again

* change name

* Onroad Brightness reimpl

* Custom Interactive Timeout reimpl

* Global Brightness Override reimpl

* keep stock

* ui: Custom Interactive Timeout

* rename

* ui: Customizable Onroad Brightness

* lint

* lint

* Revert "Global Brightness Override reimpl"

This reverts commit 53522da4f8.

* Revert "Custom Interactive Timeout reimpl"

This reverts commit 459863a9bb.

* Revert "Onroad Brightness reimpl"

This reverts commit 4092d23e57.

* fixes

* lint

* reset on show/hide

* reset on show/hide for mici

* only set if true

* wrong var

* try this out

* use clear

* starts cleanup

* wake for all visual alerts and handle timeouts

* fixup: wake for all visual alerts and handle timeouts

* handle always wake if there's an event properly

* some

* slightly more

* need this back

* Reapply "ui: Global Brightness Override (#1579)"

This reverts commit a0c10be1ff.

* do not touch light sensor logic

* override properly and clip to 30% minimum

* wrap

* lint

* update immediately

* read

* max global brightness

* rename

* gotta do it for mici too lol

* update metadata

* desc

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: DevTekVE <devtekve@gmail.com>
2026-01-19 01:42:24 -05:00
Jason Wen 3662a8e962 ui: Customizable Onroad Brightness (#1641)
* ui: Customizable Onroad Brightness

* fixes

* lint

* reset on show/hide

* reset on show/hide for mici

* only set if true

* wrong var

* try this out

* use clear

* starts cleanup

* wake for all visual alerts and handle timeouts

* fixup: wake for all visual alerts and handle timeouts

* handle always wake if there's an event properly

* some

* slightly more

* need this back

* Reapply "ui: Global Brightness Override (#1579)"

This reverts commit a0c10be1ff.

* do not touch light sensor logic

* override properly and clip to 30% minimum

* wrap

* lint

* update immediately

* read

* max global brightness

* rename

* gotta do it for mici too lol

* revert

* Revert "revert"

This reverts commit 121a082de1.

* no more

* ui

* more

* intenum

* simplify ONROAD_BRIGHTNESS_TIMER_VALUES

* no more toggle

* 15 seconds countdown for auto dark regardless

* auto dark refinement

* only consume if expired

* immediately set

* rename

* update sl metadata

* no more

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
Co-authored-by: DevTekVE <devtekve@gmail.com>
2026-01-19 01:26:16 -05:00
Copilot 49b6ef7f48 SL: Fix MaxTimeOffroad metadata unit from seconds to minutes (#1650)
* Initial plan

* Fix MaxTimeOffroad metadata unit from seconds to minutes

Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com>
2026-01-17 14:36:19 -05:00
Matt Purnell 1f9efd9311 transformations: move Cython to pure Python (#36830)
* Remove cython for transformations

* Add new test

* Switch back to program to fix mac builds

* Convert to Python instead

* Fix failing builds

* lint

* Implement conversion in pure python/numpy

* Add more tests

* Fix bugs in tests
2026-01-16 22:31:26 -08:00
Shane Smiskol 7f8dbf24e7 Cabana: fix for internal source (#36998)
* fix for internal sources from cursor

* clean up

* more

* not needed

* clean up

* sure
2026-01-15 17:18:40 -08:00
Shane Smiskol 5e4b88201e Toyota: whitelist hybrids for standstill resume behaviour (#36996)
tioyota
2026-01-15 14:38:43 -08:00
Adeeb Shihadeh 1252188b4b sensord: tighten temperature threshold (#36994)
* sensord: tighten temperature threshold

* lil more
2026-01-13 14:33:13 -08:00
DevTekVE a0c10be1ff Revert "ui: Global Brightness Override (#1579)"
This reverts commit 1eb82fcc
2026-01-10 21:24:44 +01:00
Adeeb Shihadeh 15d3a166f7 enable pyopencl on arm64 (#36990) 2026-01-09 20:49:14 -08:00
Nayan a58db66a98 sunnylink: add units to param metadata (#1643)
add units
2026-01-09 18:40:09 -05:00
Harald Schäfer f51c2aeced Modeld: less lat smoothing (#36987)
* lat is plenty smooth!

* fix
2026-01-09 15:04:33 -08:00
Harald Schäfer 3edb3243f6 SC driving (#36986)
f1d30a23-4122-400a-80a6-557502284c36/200
2026-01-09 09:16:57 -08:00
YassineYousfi c693bc1247 MacroStiff Model 🟥🟩🟦🟨 (#36972)
* 8c06e95e-d7c0-4fd9-ba02-9f0b6848785e/400

* test

* test

* test now
2026-01-05 16:14:05 -08:00
Adeeb Shihadeh 5d3ab260e1 welcome lexus ls 2026-01-05 10:15:20 -08:00
Jason Young ea64c4c0ae VW: Enable torqued (#36983) 2026-01-04 21:35:13 -05:00
Adeeb Shihadeh 84bce8ae02 rm pygame (#36982)
* rm pygame

* lil more

* cleanup

* lil more
2026-01-04 17:52:10 -08:00
Jason Young 3c5974930a cleanup SecOC release gating (#36980) 2026-01-04 17:09:26 -05:00
Adeeb Shihadeh be854df32d remove unused dbus-next package (#36979) 2026-01-04 13:46:30 -08:00
discountchubbs a966e2fcfe Merge remote-tracking branch 'origin/master' into only-chubbs
# Conflicts:
#	common/params_keys.h
#	opendbc_repo
#	selfdrive/ui/mici/onroad/model_renderer.py
#	selfdrive/ui/onroad/model_renderer.py
#	sunnypilot/modeld_v2/camera_offset_helper.py
#	sunnypilot/modeld_v2/tests/test_camera_offset_helper.py
#	sunnypilot/sunnylink/params_metadata.json
2026-01-04 08:09:26 -08:00
YassineYousfi adbf68f771 FrameReader: add hwaccel arg and clear frames_cache (#36974) 2026-01-02 11:29:45 -08:00
YassineYousfi f62177a827 FrameReader: use hwaccel auto (#36973)
* FrameReader: use hwaccel auto

* rm main block
2026-01-01 19:06:31 -08:00
Alexandre Nobuharu Sato bb40d161e8 Add back badges for keep multilanguage support (#36967) 2026-01-01 15:13:18 -08:00
Shane Smiskol 84b1f363e4 Alert for stock LKAS (#36969)
* alert stock lkas

* high

* i didn't do this
2025-12-31 12:43:36 -08:00
discountchubbs 7c9a628308 Merge remote-tracking branch 'origin/master' into only-chubbss 2025-12-31 10:36:29 -07:00
Adeeb Shihadeh a5348b8679 crcmod -> crcmod-plus (#36968) 2025-12-29 16:41:52 -08:00
discountchubbs e830c1edab Merge remote-tracking branch 'origin/only-chubbs' into only-chubbs 2025-12-29 12:16:57 -07:00
discountchubbs eb91efe2c2 Merge remote-tracking branch 'origin/master' into only-chubbs
# Conflicts:
#	opendbc_repo
#	selfdrive/ui/sunnypilot/ui_state.py
2025-12-29 12:13:05 -07:00
discountchubbs 028a4d10e7 Merge remote-tracking branch 'origin/master' into only-chubbs
# Conflicts:
#	opendbc_repo
#	selfdrive/ui/sunnypilot/ui_state.py
2025-12-29 08:40:52 -07:00
discountchubbs edf697392c # Conflicts:
#	opendbc_repo
#	selfdrive/ui/sunnypilot/layouts/settings/settings.py
#	selfdrive/ui/sunnypilot/mici/layouts/settings.py
#	selfdrive/ui/sunnypilot/ui_state.py
2025-12-29 08:40:01 -07:00
Adeeb Shihadeh 63c9a85c6a FrameReader: use HW accel if available (#36964)
* FrameReader: add macOS hw accel

* sys

* more platforms

* logging
2025-12-28 21:38:54 -08:00
Adeeb Shihadeh adf9ec5360 tools: speed up Route() (#36963)
* tools: speed up Route()

* cleanup
2025-12-28 15:39:20 -08:00
commaci-public 883d1232d3 [bot] Update Python packages (#36606)
update

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-12-28 13:00:35 -08:00
Adeeb Shihadeh b58fddb83e replay: add --benchmark mode (#36957) 2025-12-28 12:20:45 -08:00
Adeeb Shihadeh ce1491df9c tools: add LRU eviction for log cache (#36959)
* tools: add LRU for log cache

* lil more

* cleanup:

* less syscall

* manifest

* cleanup

* cleanup

* lil more

* cleanup

* lil more

* simpler

* lil more
2025-12-28 11:45:19 -08:00
Adeeb Shihadeh ea01a53711 switch from mypy to ty (#36961) 2025-12-28 10:42:49 -08:00
Adeeb Shihadeh 85cdb2ed9a fix(url_file): ensure seek position is always an integer (#36960) 2025-12-27 23:15:18 -08:00
Adeeb Shihadeh 368947c88c msgq on macOS (#36958)
* msgq on macOS

* clean that up
2025-12-27 16:49:30 -08:00
discountchubbs d991bc9bc4 hkg longitudinal: no more ramp update 2025-12-26 21:04:19 -06:00
discountchubbs f6a0a830ca planplus controls: modify planplus model recentering over sunnylink! 2025-12-26 21:00:03 -06:00
discountchubbs a97aa56d3c modeld: camera offset class 2025-12-25 17:58:10 -06:00
discountchubbs 45b9663780 SCC-V: controller smoothing 2025-12-25 16:51:19 -06:00
discountchubbs 0b384119ec modeld: configurable camera offset
Negative Values: Shears the image to the left, moving the models center to the Right.

Positive Value: Shears the image to the right, moving the models center to the Left.
2025-12-24 12:46:07 -06:00
Trey Moen 3f1f7ad89c feat(esim): remove bootstrap (#36954)
feat: remove bootstrap
2025-12-24 09:04:34 -08:00
discountchubbs 140809a564 tools: video concat 2025-12-23 18:16:33 -06:00
discountchubbs 37172a0cbc reduce threshold 2025-12-23 15:46:06 -06:00
discountchubbs 78248cdbba Update vision_controller.py 2025-12-22 21:53:54 -06:00
Adeeb Shihadeh a8cfa2e2fe bump msgq (#36950) 2025-12-21 17:22:57 -08:00
Adeeb Shihadeh c8eed43538 misc system/hardware/ cleanup (#36949)
* misc system/hardware/ cleanup

* lil more

* pc is kinda base

* lil more

* lil more

* lil more

* int

* lil more?
2025-12-21 17:02:39 -08:00
Adeeb Shihadeh 29a2f576f5 unpin raylib-python-cffi (#36948) 2025-12-21 16:49:15 -08:00
Adeeb Shihadeh 31ec0096e4 ui: fix raylib init spam (#36947)
* lil more

* not none

* don't need that either

* too spammy now?
2025-12-21 16:21:35 -08:00
Adeeb Shihadeh 8728c7dde3 killing car_specific.py, part 3 (#36946)
* mv that

* lil more

* todo

* cleanup
2025-12-21 15:42:53 -08:00
Dean Lee e9a37d99c3 Fix incorrect msgq path in prefix.h (#36943) 2025-12-20 16:33:52 -08:00
Adeeb Shihadeh 67742699cc card: move drivable gears to opendbc (#36942)
* card: move drivable gears to opendbc

* it's not none

* bump opendbc

* mypy
2025-12-20 15:22:30 -08:00
Adeeb Shihadeh de975d5af9 card: remove MockCarstate (#36941) 2025-12-20 15:04:13 -08:00
discountchubbs f5d3fd3927 tools: memory profiler 2025-12-19 13:54:53 -08:00
eFini c3143f3833 Multilang: update zh translation (#36933) 2025-12-19 09:38:23 -08:00
discountchubbs 95762333d5 # Conflicts:
#	selfdrive/ui/sunnypilot/mici/layouts/settings.py
#	selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
#	selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py
#	selfdrive/ui/sunnypilot/ui_state.py
2025-12-19 05:53:07 -08:00
Maxime Desroches 1c2f9e6190 bump version to 0.10.4 2025-12-18 23:26:59 -08:00
Maxime Desroches 654338f9c7 update release instructions 2025-12-18 22:52:14 -08:00
Maxime Desroches dfd7a8c8d7 AGNOS 16 (#36915)
* stage

* prod

* bump reset
2025-12-18 22:02:46 -08:00
Shane Smiskol bb8a5bd476 Fix slow DM onboarding (#36932)
* slow

* interesting

* check

* clean up
2025-12-18 20:50:34 -08:00
Shane Smiskol e4359e9acb api_get: use session (#36930)
* use session

* prob better for now

* todo

* same for firehose

* no more stutters!

* cmt
2025-12-18 17:15:47 -08:00
Maxime Desroches 13b8a67ae2 reset: fix button overlap (#36929)
fix
2025-12-18 16:03:45 -08:00
discountchubbs 435284427e vision turn 2025-12-18 15:58:27 -08:00
discountchubbs 02078a8d0f sync 2025-12-18 06:27:11 -08:00
discountchubbs 59e4cf4188 Merge remote-tracking branch 'origin/master' into only-chubbs
# Conflicts:
#	opendbc_repo
#	selfdrive/controls/lib/latcontrol_torque.py
2025-12-18 06:17:47 -08:00
Shane Smiskol 89d9fdca82 WifiUi: tune wifi strengths (#36923)
* tune wifi strengths

* android nor ios show none or slash. it's always 1, 2, or 3 bars

* match here

* clean up
2025-12-17 23:15:22 -08:00
Shane Smiskol a478b64ff3 WifiUi: fix clicking (#36919)
* fix

* rm

* rename

* need this in case user swipes back to starting pos

* better

* better

* clean up

* cmt
2025-12-17 21:55:31 -08:00
Shane Smiskol b3c2daf9e5 Revert "Widget: track mouse events (#36922)"
https://github.com/commaai/openpilot/pull/36920#issuecomment-3668453692

This reverts commit 792a9b715c.
2025-12-17 21:32:06 -08:00
Shane Smiskol 792a9b715c Widget: track mouse events (#36922)
* track

* fix

* rm
2025-12-17 21:30:40 -08:00
Shane Smiskol 631d6d9ef4 Widget: mouse handlers don't use bool (#36921)
* not used

* lint
2025-12-17 21:26:52 -08:00
Shane Smiskol 6249211745 Update canError alert texts (#36918)
* hmm

* hmm it's small

* can

* ?

* variant?

* unknown
2025-12-17 19:57:57 -08:00
discountchubbs b8205522f0 lateral: friction threshold 2025-12-17 17:52:13 -08:00
Shane Smiskol 2213f8f8a4 MultiOptionDialog: tap activates current option (#36911)
* works

* clean up
2025-12-17 17:10:12 -08:00
Shane Smiskol 4e85568370 MultiOptionDialog: support no default (#36912)
fix
2025-12-17 17:02:19 -08:00
Shane Smiskol 88a4f2baf1 Onboarding: no buttons if no frame (#36909)
no buttons if no frame
2025-12-17 16:27:43 -08:00
discountchubbs 4dda8f52a4 Update models.py 2025-12-16 12:02:03 -08:00
discountchubbs 5d4ae3c26e macOS metadrive support! 2025-12-15 15:52:30 -08:00
discountchubbs d62c036018 revert tune 2025-12-14 08:24:29 -08:00
discountchubbs a81044868d no snappy snap
and fluck your scroll
2025-12-13 16:58:50 -08:00
discountchubbs 029e4974c3 This is annoying. maybe we should open a pr for it 2025-12-13 16:36:24 -08:00
discountchubbs 70d722c0d1 models 2025-12-13 16:25:50 -08:00
discountchubbs afb8000bbc Merge remote-tracking branch 'origin/master' into only-chubbs
# Conflicts:
#	opendbc_repo
#	selfdrive/ui/sunnypilot/layouts/settings/settings.py
2025-12-13 07:12:55 -08:00
discountchubbs f0f7ab0f35 Research report 2025-12-13 07:10:54 -08:00
discountchubbs 92767345f2 Revert "mem test"
This reverts commit c801918c89.
2025-12-11 07:46:51 -08:00
discountchubbs f6799f686b add the tune here 2025-12-11 06:38:35 -08:00
discountchubbs c801918c89 mem test 2025-12-09 18:15:54 -08:00
discountchubbs 6919407d2c Update profile_params.py 2025-12-09 17:09:59 -08:00
discountchubbs 898f782f86 increase buffer to 2KiB 2025-12-09 06:02:39 -08:00
discountchubbs 68dc50546e more descriptive 2025-12-08 19:20:58 -08:00
discountchubbs 4c57ffeca2 Merge remote-tracking branch 'origin/double-policy' into only-chubbs 2025-12-08 16:40:59 -08:00
James Vecellio-Grant 639c1fdf7d Merge branch 'master' into double-policy 2025-12-08 16:40:47 -08:00
discountchubbs 90ebbc6232 Merge remote-tracking branch 'origin/mici-sunnylink' into only-chubbs 2025-12-08 16:35:41 -08:00
discountchubbs a6e8048ed7 top 30 2025-12-08 16:28:40 -08:00
discountchubbs bfae9de4b2 random 99999 2025-12-08 12:32:49 -08:00
nayan e9c8d1f8ff add uploader 2025-12-08 14:22:02 -05:00
nayan 35e26e5a4e fix 2025-12-08 14:22:02 -05:00
nayan 26abc81892 icons 2025-12-08 14:22:02 -05:00
nayan d58805156c sunnylink-mici 2025-12-08 14:22:01 -05:00
discountchubbs a3af62629d tools 2025-12-08 06:12:00 -08:00
discountchubbs b737989e64 not needed 2025-12-08 05:54:50 -08:00
James Vecellio-Grant 5fbc358fd5 Merge branch 'paramwatcher' into watcher 2025-12-08 05:54:18 -08:00
discountchubbs ce30d815f7 except "." 2025-12-08 05:53:52 -08:00
James Vecellio fdde1aa6a1 this is hindsight 2025-12-07 19:26:36 -08:00
discountchubbs 961b2a2d30 drive back the darkness 2025-12-07 18:20:50 -08:00
discountchubbs f3d39d481a wat 2025-12-07 17:13:58 -08:00
James Vecellio-Grant 6e037d80ff Merge branch 'paramwatcher' into watcher 2025-12-07 17:13:06 -08:00
discountchubbs 907bc5cf06 pyzmq considered... rejected.
I created a service handler in pyzmq and memory tested and it was in the MegaBytes, comparable to direct Params access, fudge that.
2025-12-07 17:12:39 -08:00
James Vecellio-Grant b3ff268f89 Merge branch 'paramwatcher' into watcher 2025-12-07 14:05:37 -08:00
discountchubbs 42e08515e6 make the link go to LxCx 2025-12-07 12:22:15 -08:00
discountchubbs d0ec46dc5d cp 2025-12-07 12:14:54 -08:00
discountchubbs 48a8802298 cp analysis needs high level 2025-12-07 12:12:43 -08:00
discountchubbs 79971b9eb2 move limitations to end 2025-12-07 12:11:49 -08:00
discountchubbs 7ba21f9f1b 2025 2025-12-07 11:57:27 -08:00
discountchubbs 6b4118ab27 dates 2025-12-07 11:55:27 -08:00
discountchubbs 0844424ad1 update 2025-12-07 11:47:14 -08:00
James Vecellio-Grant 5901c9b41f Merge branch 'paramwatcher' into watcher 2025-12-07 11:45:20 -08:00
discountchubbs d52ce19c15 update readme 2025-12-07 11:44:34 -08:00
discountchubbs 05cc9a14e2 reset thread 2025-12-07 11:33:43 -08:00
discountchubbs 18f8956e0e params 2025-12-07 11:23:49 -08:00
discountchubbs 0aa6f22c26 watch 2025-12-07 11:15:58 -08:00
James Vecellio-Grant c90f262ce7 Merge branch 'paramwatcher' into watcher 2025-12-07 11:08:48 -08:00
discountchubbs e8ee5a23f0 my little soda pop 2025-12-07 11:08:16 -08:00
discountchubbs 4a189f828a oopsie 2025-12-07 10:13:36 -08:00
James Vecellio-Grant 072e18faef Merge branch 'paramwatcher' into watcher 2025-12-07 10:12:56 -08:00
James Vecellio 3b1fddfde9 60% comparison for slow computers 2025-12-07 10:12:14 -08:00
discountchubbs bddec6971e debounce 2025-12-07 10:06:23 -08:00
discountchubbs 34e02b6ae5 Revert "Merge remote-tracking branch 'origin/paramwatcher' into watcher"
This reverts commit c98cc5d40a, reversing
changes made to 4a0d8063e5.
2025-12-07 10:03:04 -08:00
discountchubbs c98cc5d40a Merge remote-tracking branch 'origin/paramwatcher' into watcher 2025-12-07 10:02:55 -08:00
discountchubbs 4a0d8063e5 Merge remote-tracking branch 'origin/paramwatcher' into watcher 2025-12-07 10:02:41 -08:00
discountchubbs e2e52bcccb Merge remote-tracking branch 'origin/paramwatcher' into paramwatcher 2025-12-07 09:59:57 -08:00
discountchubbs ccf86b7b72 cb 2025-12-07 09:59:47 -08:00
James Vecellio-Grant 483894cfc8 Merge branch 'master' into watcher 2025-12-07 09:59:04 -08:00
James Vecellio-Grant a678554122 Merge branch 'master' into paramwatcher 2025-12-07 09:58:35 -08:00
discountchubbs bfd3eab260 rm 2025-12-07 09:56:45 -08:00
discountchubbs f5aedbce6e unit/int tests 2025-12-07 09:45:18 -08:00
discountchubbs 4f860dd397 Merge remote-tracking branch 'origin/watcher' into watcher 2025-12-07 08:10:59 -08:00
discountchubbs f308d9ab17 markdown 2025-12-07 08:10:50 -08:00
James Vecellio-Grant 9226222ad4 Merge branch 'master' into watcher 2025-12-06 14:44:23 -08:00
discountchubbs 3e317a8b4d give me ALL the params access. limit sys reading by 500x 2025-12-06 14:43:45 -08:00
nayan be9f007a2e show contributor tier 2025-12-06 17:33:35 -05:00
nayan 22b7849771 fix backup/restore status 2025-12-06 17:25:04 -05:00
nayan 40f2030048 flippity floppity 2025-12-06 16:53:15 -05:00
nayan 93b8395c7a Merge remote-tracking branch 'origin/master' into mici-sunnylink
# Conflicts:
#	sunnypilot/sunnylink/sunnylink_state.py
2025-12-06 13:25:38 -05:00
James Vecellio 0eae4e0b3b layout 2025-12-05 19:44:19 -08:00
James Vecellio 37ffa5ed21 layout 2025-12-05 19:44:14 -08:00
James Vecellio 05e3eaf2fc certified freak 2025-12-05 15:59:13 -08:00
discountchubbs c8fc344d68 watch this paramwatcher 2025-12-05 13:17:04 -08:00
James Vecellio-Grant 264948e5ff Update param_watcher.py 2025-12-05 08:55:02 -08:00
James Vecellio-Grant 9d87beac8e Merge branch 'master' into watcher 2025-12-05 08:52:18 -08:00
James Vecellio-Grant 2e0bc80f94 Update param_watcher.py 2025-12-05 08:50:31 -08:00
James Vecellio-Grant 4b8781886a Update ui_state.py 2025-12-05 08:43:08 -08:00
James Vecellio-Grant 97edff5e5c Merge branch 'master' into watcher 2025-12-04 06:06:29 -08:00
discountchubbs a81570a6c2 file system watcher 👀 2025-12-03 22:23:37 -08:00
James Vecellio-Grant 5620e60aa1 Merge branch 'master' into double-policy 2025-12-03 06:04:19 -08:00
nayan db16bc6615 lint 2025-12-02 12:36:44 -05:00
nayan f1eafe56d7 cleanup 2025-12-02 12:29:53 -05:00
nayan 7d4993cc42 Merge branch 'py-sunnylink' into mici-sunnylink 2025-12-02 12:23:04 -05:00
nayan 0f6ad56fb9 init sunnylink panels 2025-12-02 12:22:53 -05:00
nayan f5b4f3b206 Merge remote-tracking branch 'origin/master' into py-sunnylink 2025-12-01 11:05:41 -05:00
James Vecellio-Grant 4e8060c4f8 modifucatuins 2025-12-01 00:04:19 +00:00
James Vecellio-Grant 5212203cc2 Merge branch 'master' into double-policy 2025-11-30 07:36:16 -08:00
Jason Wen f1e359294f Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into py-sunnylink
# Conflicts:
#	selfdrive/ui/sunnypilot/ui_state.py
2025-11-30 00:58:21 -05:00
James Vecellio-Grant a9d2b9be30 Merge branch 'master' into double-policy 2025-11-29 17:38:44 -08:00
discountchubbs 718cd3f685 v13 2025-11-28 20:31:24 -08:00
James Vecellio-Grant 2d33d368f3 conditional concatenation 2025-11-29 04:09:10 +00:00
nayan fb1b0655c4 Merge remote-tracking branch 'origin/master' into py-sunnylink
# Conflicts:
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/list_view.py
#	system/ui/sunnypilot/widgets/toggle.py
2025-11-22 13:33:49 -05:00
nayan 01842dbdca fetch only when connected to network 2025-11-22 13:31:15 -05:00
nayan 5957db94f6 use gui_app.sunnypilot_ui() 2025-11-21 17:55:00 -05:00
nayan ef1810913e Merge branch 'rl-sp-toggles' into py-sunnylink 2025-11-21 17:51:03 -05:00
nayan ed775185f2 use gui_app.sunnypilot_ui() 2025-11-21 17:49:27 -05:00
nayan 7bbbc6588e Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sp-toggles 2025-11-21 17:42:23 -05:00
nayan e68c65d15d Merge remote-tracking branch 'origin/master' into rl-sp-toggles 2025-11-21 17:40:10 -05:00
Jason Wen 0db8722221 Merge branch 'master' into ui-gui-app-ext 2025-11-21 17:24:14 -05:00
Jason Wen a33497ed19 add to readme 2025-11-21 16:42:59 -05:00
Jason Wen 91f2bf3459 ui: GuiApplicationExt 2025-11-21 16:23:01 -05:00
Jason Wen 7fad2fc189 Merge branch 'master' into rl-sp-toggles 2025-11-21 15:55:34 -05:00
nayan 9f303e9ea9 Merge remote-tracking branch 'origin/master' into py-sunnylink 2025-11-21 15:38:52 -05:00
Jason Wen 0613442ac9 Merge branch 'master' into rl-sp-toggles 2025-11-21 15:00:14 -05:00
nayan e6f5aae246 remove padding from line separator.
like, WHY? 😩😩
2025-11-20 18:05:12 -05:00
nayan 7032e4a972 add show_description method 2025-11-20 18:00:44 -05:00
nayan 5b03369a8f listitem -> listitemsp 2025-11-20 17:56:26 -05:00
nayan 1e0564b484 this 2025-11-20 08:05:20 -05:00
nayan eb94abaa14 better padding 2025-11-19 23:44:05 -05:00
nayan c270268d3a Merge branch 'py-ui-state-sp' into py-sunnylink
# Conflicts:
#	selfdrive/ui/sunnypilot/ui_state.py
2025-11-18 19:11:26 -05:00
nayan 4820265268 better 2025-11-18 19:09:46 -05:00
nayan 01aa6c4204 param to control stock vs sp ui 2025-11-18 18:51:52 -05:00
nayan 6d6c975bfb cloudlog & ruff 2025-11-18 16:34:32 -05:00
nayan accf09c34e poll from ui_state_sp 2025-11-18 16:28:55 -05:00
nayan 3a3f7a3843 Merge branch 'refs/heads/py-ui-state-sp' into py-sunnylink 2025-11-18 16:27:54 -05:00
nayan 21beea51ec introducing ui_state_sp for py 2025-11-18 16:26:48 -05:00
nayan 06c1557785 sunnylink state 2025-11-18 16:20:23 -05:00
nayan 423a7d2ed0 fix ui preview 2025-11-16 11:15:28 -05:00
nayan e4e10d4b87 fix callback 2025-11-16 11:15:22 -05:00
nayan 362e9ce04b sp raylib preview 2025-11-16 09:53:28 -05:00
nayan 3946e643f6 optimizations 2025-11-16 09:29:58 -05:00
nayan 0c37a38596 Lint 2025-11-16 09:29:58 -05:00
nayan 9c5acf61c0 SP Toggles 2025-11-16 09:29:58 -05:00
nayan 121b304fe0 init styles 2025-11-16 09:29:58 -05:00
nayan 47d848293b param to control stock vs sp ui 2025-11-16 09:29:58 -05:00
201 changed files with 5614 additions and 3333 deletions
-1
View File
@@ -108,7 +108,6 @@ jobs:
build_mac: build_mac:
name: build macOS name: build macOS
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
if: false # There'll be one day that this works. That day is not today.
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
Vendored
+1 -1
View File
@@ -22,7 +22,7 @@ shopt -s huponexit # kill all child processes when the shell exits
export CI=1 export CI=1
export PYTHONWARNINGS=error export PYTHONWARNINGS=error
export LOGPRINT=debug #export LOGPRINT=debug # this has gotten too spammy...
export TEST_DIR=${env.TEST_DIR} export TEST_DIR=${env.TEST_DIR}
export SOURCE_DIR=${env.SOURCE_DIR} export SOURCE_DIR=${env.SOURCE_DIR}
export GIT_BRANCH=${env.GIT_BRANCH} export GIT_BRANCH=${env.GIT_BRANCH}
+4
View File
@@ -1,3 +1,7 @@
Version 0.10.4 (2026-02-17)
========================
* Lexus LS 2018 support thanks to Hacheoy!
Version 0.10.3 (2025-12-17) Version 0.10.3 (2025-12-17)
======================== ========================
* New driving model #36249 * New driving model #36249
+1
View File
@@ -87,6 +87,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
laneChange @50; laneChange @50;
lowMemory @51; lowMemory @51;
stockAeb @52; stockAeb @52;
stockLkas @98;
ldw @53; ldw @53;
carUnrecognized @54; carUnrecognized @54;
invalidLkasSetting @55; invalidLkasSetting @55;
+1 -1
View File
@@ -13,7 +13,7 @@ from typing import Optional, List, Union, Dict
from cereal import log from cereal import log
from cereal.services import SERVICE_LIST from cereal.services import SERVICE_LIST
from openpilot.common.util import MovingAverage from openpilot.common.utils import MovingAverage
NO_TRAVERSAL_LIMIT = 2**64-1 NO_TRAVERSAL_LIMIT = 2**64-1
+2 -1
View File
@@ -33,7 +33,8 @@ void zmq_to_msgq(const std::vector<std::string> &endpoints, const std::string &i
for (auto endpoint : endpoints) { for (auto endpoint : endpoints) {
auto pub_sock = new MSGQPubSocket(); auto pub_sock = new MSGQPubSocket();
auto sub_sock = new ZMQSubSocket(); auto sub_sock = new ZMQSubSocket();
pub_sock->connect(pub_context.get(), endpoint); size_t queue_size = services.at(endpoint).queue_size;
pub_sock->connect(pub_context.get(), endpoint, true, queue_size);
sub_sock->connect(sub_context.get(), endpoint, ip, false); sub_sock->connect(sub_context.get(), endpoint, ip, false);
poller->registerSocket(sub_sock); poller->registerSocket(sub_sock);
+3 -1
View File
@@ -2,6 +2,7 @@
#include <cassert> #include <cassert>
#include "cereal/services.h"
#include "common/util.h" #include "common/util.h"
extern ExitHandler do_exit; extern ExitHandler do_exit;
@@ -108,7 +109,8 @@ void MsgqToZmq::zmqMonitorThread() {
if (++pair.connected_clients == 1) { if (++pair.connected_clients == 1) {
// Create new MSGQ subscriber socket and map to ZMQ publisher // Create new MSGQ subscriber socket and map to ZMQ publisher
pair.sub_sock = std::make_unique<MSGQSubSocket>(); pair.sub_sock = std::make_unique<MSGQSubSocket>();
pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1"); size_t queue_size = services.at(pair.endpoint).queue_size;
pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1", false, true, queue_size);
sub2pub[pair.sub_sock.get()] = pair.pub_sock.get(); sub2pub[pair.sub_sock.get()] = pair.pub_sock.get();
registerSockets(); registerSockets();
} }
+1 -6
View File
@@ -19,11 +19,6 @@ if GetOption('extras'):
# Cython bindings # Cython bindings
params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11']) params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11'])
SConscript([ common_python = [params_python]
'transformations/SConscript',
])
Import('transformations_python')
common_python = [params_python, transformations_python]
Export('common_python') Export('common_python')
+2 -2
View File
@@ -18,8 +18,8 @@ class Api:
return self.service.get_token(payload_extra, expiry_hours) return self.service.get_token(payload_extra, expiry_hours)
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params): def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params):
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, session, **params)
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
+4 -2
View File
@@ -51,7 +51,7 @@ class BaseApi:
ascii_encoded_text = normalized_text.encode('ascii', 'ignore') ascii_encoded_text = normalized_text.encode('ascii', 'ignore')
return ascii_encoded_text.decode() return ascii_encoded_text.decode()
def api_get(self, endpoint, method='GET', timeout=None, access_token=None, json=None, **params): def api_get(self, endpoint, method='GET', timeout=None, access_token=None, session=None, json=None, **params):
headers = {} headers = {}
if access_token is not None: if access_token is not None:
headers['Authorization'] = "JWT " + access_token headers['Authorization'] = "JWT " + access_token
@@ -59,7 +59,9 @@ class BaseApi:
version = self.remove_non_ascii_chars(get_version()) version = self.remove_non_ascii_chars(get_version())
headers['User-Agent'] = self.user_agent + version headers['User-Agent'] = self.user_agent + version
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params) # TODO: add session to Api
req = requests if session is None else session
return req.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
@staticmethod @staticmethod
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
+6 -6
View File
@@ -4,27 +4,27 @@ from openpilot.common.utils import run_cmd, run_cmd_default
@cache @cache
def get_commit(cwd: str = None, branch: str = "HEAD") -> str: def get_commit(cwd: str | None = None, branch: str = "HEAD") -> str:
return run_cmd_default(["git", "rev-parse", branch], cwd=cwd) return run_cmd_default(["git", "rev-parse", branch], cwd=cwd)
@cache @cache
def get_commit_date(cwd: str = None, commit: str = "HEAD") -> str: def get_commit_date(cwd: str | None = None, commit: str = "HEAD") -> str:
return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit], cwd=cwd) return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit], cwd=cwd)
@cache @cache
def get_short_branch(cwd: str = None) -> str: def get_short_branch(cwd: str | None = None) -> str:
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd) return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
@cache @cache
def get_branch(cwd: str = None) -> str: def get_branch(cwd: str | None = None) -> str:
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=cwd) return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=cwd)
@cache @cache
def get_origin(cwd: str = None) -> str: def get_origin(cwd: str | None = None) -> str:
try: try:
local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"], cwd=cwd) local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"], cwd=cwd)
tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"], cwd=cwd) tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"], cwd=cwd)
@@ -34,7 +34,7 @@ def get_origin(cwd: str = None) -> str:
@cache @cache
def get_normalized_origin(cwd: str = None) -> str: def get_normalized_origin(cwd: str | None = None) -> str:
return get_origin(cwd) \ return get_origin(cwd) \
.replace("git@", "", 1) \ .replace("git@", "", 1) \
.replace(".git", "", 1) \ .replace(".git", "", 1) \
+1 -1
View File
@@ -1 +1 @@
#define DEFAULT_MODEL "Dark Souls 2 (Default)" #define DEFAULT_MODEL "WMI (Default)"
-1
View File
@@ -168,7 +168,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}}, {"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}}, {"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffControl", {PERSISTENT | BACKUP, BOOL}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}}, {"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}}, {"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
+3 -9
View File
@@ -3,15 +3,9 @@ from numbers import Number
class PIDController: class PIDController:
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100): def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
self._k_p = k_p self._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p
self._k_i = k_i self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i
self._k_d = k_d self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d
if isinstance(self._k_p, Number):
self._k_p = [[0], [self._k_p]]
if isinstance(self._k_i, Number):
self._k_i = [[0], [self._k_i]]
if isinstance(self._k_d, Number):
self._k_d = [[0], [self._k_d]]
self.set_limits(pos_limit, neg_limit) self.set_limits(pos_limit, neg_limit)
+5 -1
View File
@@ -13,7 +13,11 @@ public:
if (prefix.empty()) { if (prefix.empty()) {
prefix = util::random_string(15); prefix = util::random_string(15);
} }
msgq_path = Path::shm_path() + "/" + prefix; #ifdef __APPLE__
msgq_path = "/tmp/msgq_" + prefix;
#else
msgq_path = "/dev/shm/msgq_" + prefix;
#endif
bool ret = util::create_directories(msgq_path, 0777); bool ret = util::create_directories(msgq_path, 0777);
assert(ret); assert(ret);
setenv("OPENPILOT_PREFIX", prefix.c_str(), 1); setenv("OPENPILOT_PREFIX", prefix.c_str(), 1);
+4 -2
View File
@@ -1,4 +1,5 @@
import os import os
import platform
import shutil import shutil
import uuid import uuid
@@ -9,9 +10,10 @@ from openpilot.system.hardware.hw import Paths
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
class OpenpilotPrefix: class OpenpilotPrefix:
def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): def __init__(self, prefix: str | None = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False):
self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15])
self.msgq_path = os.path.join(Paths.shm_path(), "msgq_" + self.prefix) shm_path = "/tmp" if platform.system() == "Darwin" else "/dev/shm"
self.msgq_path = os.path.join(shm_path, "msgq_" + self.prefix)
self.create_dirs_on_enter = create_dirs_on_enter self.create_dirs_on_enter = create_dirs_on_enter
self.clean_dirs_on_exit = clean_dirs_on_exit self.clean_dirs_on_exit = clean_dirs_on_exit
self.shared_download_cache = shared_download_cache self.shared_download_cache = shared_download_cache
+1 -1
View File
@@ -6,7 +6,7 @@ import time
from setproctitle import getproctitle from setproctitle import getproctitle
from openpilot.common.util import MovingAverage from openpilot.common.utils import MovingAverage
from openpilot.system.hardware import PC from openpilot.system.hardware import PC
-5
View File
@@ -1,5 +0,0 @@
Import('env', 'envCython')
transformations = env.Library('transformations', ['orientation.cc', 'coordinates.cc'])
transformations_python = envCython.Program('transformations.so', 'transformations.pyx')
Export('transformations', 'transformations_python')
@@ -102,3 +102,36 @@ class TestNED:
np.testing.assert_allclose(converter.ned2ecef(ned_offsets_batch), np.testing.assert_allclose(converter.ned2ecef(ned_offsets_batch),
ecef_positions_offset_batch, ecef_positions_offset_batch,
rtol=1e-9, atol=1e-7) rtol=1e-9, atol=1e-7)
def test_errors(self):
# Test wrong shape/type for geodetic2ecef
# numpy_wrap raises IndexError for scalar input
with np.testing.assert_raises(IndexError):
coord.geodetic2ecef(1.0)
with np.testing.assert_raises_regex(ValueError, "Geodetic must be size 3"):
coord.geodetic2ecef([0, 0])
with np.testing.assert_raises_regex(ValueError, "Geodetic must be size 3"):
coord.geodetic2ecef([0, 0, 0, 0])
with np.testing.assert_raises(TypeError):
coord.geodetic2ecef(['a', 'b', 'c'])
# Test LocalCoord constructor errors
with np.testing.assert_raises(ValueError):
coord.LocalCoord.from_geodetic([0, 0])
with np.testing.assert_raises(ValueError):
coord.LocalCoord.from_geodetic(1)
with np.testing.assert_raises(TypeError):
coord.LocalCoord.from_geodetic(['a', 'b', 'c'])
# Test wrong shape/type for ecef2geodetic
with np.testing.assert_raises(ValueError):
coord.ecef2geodetic([1, 2])
with np.testing.assert_raises(ValueError):
coord.ecef2geodetic([1, 2, 3, 4])
with np.testing.assert_raises(IndexError):
coord.ecef2geodetic(1.0)
@@ -1,4 +1,5 @@
import numpy as np import numpy as np
import pytest
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \ from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
rot2quat, quat2rot, \ rot2quat, quat2rot, \
@@ -59,3 +60,32 @@ class TestOrientation:
np.testing.assert_allclose(ned_eulers[i], ned_euler_from_ecef(ecef_positions[i], eulers[i]), rtol=1e-7) np.testing.assert_allclose(ned_eulers[i], ned_euler_from_ecef(ecef_positions[i], eulers[i]), rtol=1e-7)
#np.testing.assert_allclose(eulers[i], ecef_euler_from_ned(ecef_positions[i], ned_eulers[i]), rtol=1e-7) #np.testing.assert_allclose(eulers[i], ecef_euler_from_ned(ecef_positions[i], ned_eulers[i]), rtol=1e-7)
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7) # np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
def test_inputs(self):
with pytest.raises(ValueError):
euler2quat([1, 2])
with pytest.raises(ValueError):
quat2rot([1, 2, 3])
with pytest.raises(IndexError):
rot2quat(np.zeros((2, 2)))
def test_euler_rot_consistency(self):
rpy = [0.1, 0.2, 0.3]
R = euler2rot(rpy)
# R -> q -> R
q = rot2quat(R)
R_new = quat2rot(q)
np.testing.assert_allclose(R, R_new, atol=1e-15)
# q -> R -> Euler (quat2euler) -> R
rpy_new = quat2euler(q)
R_new2 = euler2rot(rpy_new)
np.testing.assert_allclose(R, R_new2, atol=1e-15)
# R -> Euler (rot2euler) -> R
rpy_from_rot = rot2euler(R)
R_new3 = euler2rot(rpy_from_rot)
np.testing.assert_allclose(R, R_new3, atol=1e-15)
@@ -1,72 +0,0 @@
# cython: language_level=3
from libcpp cimport bool
cdef extern from "orientation.cc":
pass
cdef extern from "orientation.hpp":
cdef cppclass Quaternion "Eigen::Quaterniond":
Quaternion()
Quaternion(double, double, double, double)
double w()
double x()
double y()
double z()
cdef cppclass Vector3 "Eigen::Vector3d":
Vector3()
Vector3(double, double, double)
double operator()(int)
cdef cppclass Matrix3 "Eigen::Matrix3d":
Matrix3()
Matrix3(double*)
double operator()(int, int)
Quaternion euler2quat(const Vector3 &)
Vector3 quat2euler(const Quaternion &)
Matrix3 quat2rot(const Quaternion &)
Quaternion rot2quat(const Matrix3 &)
Vector3 rot2euler(const Matrix3 &)
Matrix3 euler2rot(const Vector3 &)
Matrix3 rot_matrix(double, double, double)
Vector3 ecef_euler_from_ned(const ECEF &, const Vector3 &)
Vector3 ned_euler_from_ecef(const ECEF &, const Vector3 &)
cdef extern from "coordinates.cc":
cdef struct ECEF:
double x
double y
double z
cdef struct NED:
double n
double e
double d
cdef struct Geodetic:
double lat
double lon
double alt
bool radians
ECEF geodetic2ecef(const Geodetic &)
Geodetic ecef2geodetic(const ECEF &)
cdef cppclass LocalCoord_c "LocalCoord":
Matrix3 ned2ecef_matrix
Matrix3 ecef2ned_matrix
LocalCoord_c(const Geodetic &, const ECEF &)
LocalCoord_c(const Geodetic &)
LocalCoord_c(const ECEF &)
NED ecef2ned(const ECEF &)
ECEF ned2ecef(const NED &)
NED geodetic2ned(const Geodetic &)
Geodetic ned2geodetic(const NED &)
cdef extern from "coordinates.hpp":
pass
+342
View File
@@ -0,0 +1,342 @@
import numpy as np
# Constants
a = 6378137.0
b = 6356752.3142
esq = 6.69437999014e-3
e1sq = 6.73949674228e-3
def geodetic2ecef_single(g):
"""
Convert geodetic coordinates (latitude, longitude, altitude) to ECEF.
"""
try:
if len(g) != 3:
raise ValueError("Geodetic must be size 3")
except TypeError:
raise ValueError("Geodetic must be a sequence of length 3") from None
lat, lon, alt = g
lat = np.radians(lat)
lon = np.radians(lon)
xi = np.sqrt(1.0 - esq * np.sin(lat)**2)
x = (a / xi + alt) * np.cos(lat) * np.cos(lon)
y = (a / xi + alt) * np.cos(lat) * np.sin(lon)
z = (a / xi * (1.0 - esq) + alt) * np.sin(lat)
return np.array([x, y, z])
def ecef2geodetic_single(e):
"""
Convert ECEF to geodetic coordinates using Ferrari's solution.
"""
x, y, z = e
r = np.sqrt(x**2 + y**2)
Esq = a**2 - b**2
F = 54 * b**2 * z**2
G = r**2 + (1 - esq) * z**2 - esq * Esq
C = (esq**2 * F * r**2) / (G**3)
S = np.cbrt(1 + C + np.sqrt(C**2 + 2 * C))
P = F / (3 * (S + 1 / S + 1)**2 * G**2)
Q = np.sqrt(1 + 2 * esq**2 * P)
r_0 = -(P * esq * r) / (1 + Q) + np.sqrt(0.5 * a**2 * (1 + 1.0 / Q) - P * (1 - esq) * z**2 / (Q * (1 + Q)) - 0.5 * P * r**2)
U = np.sqrt((r - esq * r_0)**2 + z**2)
V = np.sqrt((r - esq * r_0)**2 + (1 - esq) * z**2)
Z_0 = b**2 * z / (a * V)
h = U * (1 - b**2 / (a * V))
lat = np.arctan((z + e1sq * Z_0) / r)
lon = np.arctan2(y, x)
return np.array([np.degrees(lat), np.degrees(lon), h])
def euler2quat_single(euler):
"""
Convert Euler angles (roll, pitch, yaw) to a quaternion.
Rotation order: Z-Y-X (yaw, pitch, roll).
"""
phi, theta, psi = euler
c_phi, s_phi = np.cos(phi / 2), np.sin(phi / 2)
c_theta, s_theta = np.cos(theta / 2), np.sin(theta / 2)
c_psi, s_psi = np.cos(psi / 2), np.sin(psi / 2)
w = c_phi * c_theta * c_psi + s_phi * s_theta * s_psi
x = s_phi * c_theta * c_psi - c_phi * s_theta * s_psi
y = c_phi * s_theta * c_psi + s_phi * c_theta * s_psi
z = c_phi * c_theta * s_psi - s_phi * s_theta * c_psi
if w < 0:
return np.array([-w, -x, -y, -z])
return np.array([w, x, y, z])
def quat2euler_single(q):
"""
Convert a quaternion to Euler angles (roll, pitch, yaw).
"""
w, x, y, z = q
gamma = np.arctan2(2 * (w * x + y * z), 1 - 2 * (x**2 + y**2))
sin_arg = 2 * (w * y - z * x)
sin_arg = np.clip(sin_arg, -1.0, 1.0)
theta = np.arcsin(sin_arg)
psi = np.arctan2(2 * (w * z + x * y), 1 - 2 * (y**2 + z**2))
return np.array([gamma, theta, psi])
def quat2rot_single(q):
"""
Convert a quaternion to a 3x3 rotation matrix.
"""
w, x, y, z = q
xx, yy, zz = x * x, y * y, z * z
xy, xz, yz = x * y, x * z, y * z
wx, wy, wz = w * x, w * y, w * z
mat = np.array([
[1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy)],
[2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx)],
[2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)]
])
return mat
def rot2quat_single(rot):
"""
Convert a 3x3 rotation matrix to a quaternion.
"""
trace = np.trace(rot)
if trace > 0:
s = 0.5 / np.sqrt(trace + 1.0)
w = 0.25 / s
x = (rot[2, 1] - rot[1, 2]) * s
y = (rot[0, 2] - rot[2, 0]) * s
z = (rot[1, 0] - rot[0, 1]) * s
else:
if rot[0, 0] > rot[1, 1] and rot[0, 0] > rot[2, 2]:
s = 2.0 * np.sqrt(1.0 + rot[0, 0] - rot[1, 1] - rot[2, 2])
w = (rot[2, 1] - rot[1, 2]) / s
x = 0.25 * s
y = (rot[0, 1] + rot[1, 0]) / s
z = (rot[0, 2] + rot[2, 0]) / s
elif rot[1, 1] > rot[2, 2]:
s = 2.0 * np.sqrt(1.0 + rot[1, 1] - rot[0, 0] - rot[2, 2])
w = (rot[0, 2] - rot[2, 0]) / s
x = (rot[0, 1] + rot[1, 0]) / s
y = 0.25 * s
z = (rot[1, 2] + rot[2, 1]) / s
else:
s = 2.0 * np.sqrt(1.0 + rot[2, 2] - rot[0, 0] - rot[1, 1])
w = (rot[1, 0] - rot[0, 1]) / s
x = (rot[0, 2] + rot[2, 0]) / s
y = (rot[1, 2] + rot[2, 1]) / s
z = 0.25 * s
if w < 0:
return np.array([-w, -x, -y, -z])
return np.array([w, x, y, z])
def euler2rot_single(euler):
"""
Convert Euler angles (roll, pitch, yaw) to a 3x3 rotation matrix.
Rotation order: Z-Y-X (yaw, pitch, roll).
"""
phi, theta, psi = euler
cx, sx = np.cos(phi), np.sin(phi)
cy, sy = np.cos(theta), np.sin(theta)
cz, sz = np.cos(psi), np.sin(psi)
Rx = np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]])
Ry = np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]])
Rz = np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]])
return Rz @ Ry @ Rx
def rot2euler_single(rot):
"""
Convert a 3x3 rotation matrix to Euler angles (roll, pitch, yaw).
"""
return quat2euler_single(rot2quat_single(rot))
def rot_matrix(roll, pitch, yaw):
"""
Create a 3x3 rotation matrix from roll, pitch, and yaw angles.
"""
return euler2rot_single([roll, pitch, yaw])
def axis_angle_to_rot(axis, angle):
"""
Convert an axis-angle representation to a 3x3 rotation matrix.
"""
c = np.cos(angle / 2)
s = np.sin(angle / 2)
q = np.array([c, s*axis[0], s*axis[1], s*axis[2]])
return quat2rot_single(q)
class LocalCoord:
"""
A class to handle conversions between ECEF and local NED coordinates.
"""
def __init__(self, geodetic=None, ecef=None):
"""
Initialize LocalCoord with either geodetic or ECEF coordinates.
"""
if geodetic is not None:
self.init_ecef = geodetic2ecef_single(geodetic)
lat, lon, _ = geodetic
elif ecef is not None:
self.init_ecef = np.array(ecef)
lat, lon, _ = ecef2geodetic_single(ecef)
else:
raise ValueError("Must provide geodetic or ecef")
lat = np.radians(lat)
lon = np.radians(lon)
self.ned2ecef_matrix = np.array([
[-np.sin(lat) * np.cos(lon), -np.sin(lon), -np.cos(lat) * np.cos(lon)],
[-np.sin(lat) * np.sin(lon), np.cos(lon), -np.cos(lat) * np.sin(lon)],
[np.cos(lat), 0, -np.sin(lat)]
])
self.ecef2ned_matrix = self.ned2ecef_matrix.T
@classmethod
def from_geodetic(cls, geodetic):
"""
Create a LocalCoord instance from geodetic coordinates.
"""
return cls(geodetic=geodetic)
@classmethod
def from_ecef(cls, ecef):
"""
Create a LocalCoord instance from ECEF coordinates.
"""
return cls(ecef=ecef)
def ecef2ned_single(self, ecef):
"""
Convert a single ECEF point to NED coordinates relative to the origin.
"""
return self.ecef2ned_matrix @ (ecef - self.init_ecef)
def ned2ecef_single(self, ned):
"""
Convert a single NED point to ECEF coordinates.
"""
return self.ned2ecef_matrix @ ned + self.init_ecef
def geodetic2ned_single(self, geodetic):
"""
Convert a single geodetic point to NED coordinates.
"""
ecef = geodetic2ecef_single(geodetic)
return self.ecef2ned_single(ecef)
def ned2geodetic_single(self, ned):
"""
Convert a single NED point to geodetic coordinates.
"""
ecef = self.ned2ecef_single(ned)
return ecef2geodetic_single(ecef)
@property
def ned_from_ecef_matrix(self):
"""
Returns the rotation matrix from ECEF to NED coordinates.
"""
return self.ecef2ned_matrix
@property
def ecef_from_ned_matrix(self):
"""
Returns the rotation matrix from NED to ECEF coordinates.
"""
return self.ned2ecef_matrix
def ecef_euler_from_ned_single(ecef_init, ned_pose):
"""
Convert NED Euler angles (roll, pitch, yaw) at a given ECEF origin
to equivalent ECEF Euler angles.
"""
converter = LocalCoord(ecef=ecef_init)
zero = np.array(ecef_init)
x0 = converter.ned2ecef_single([1, 0, 0]) - zero
y0 = converter.ned2ecef_single([0, 1, 0]) - zero
z0 = converter.ned2ecef_single([0, 0, 1]) - zero
phi, theta, psi = ned_pose
x1 = axis_angle_to_rot(z0, psi) @ x0
y1 = axis_angle_to_rot(z0, psi) @ y0
z1 = axis_angle_to_rot(z0, psi) @ z0
x2 = axis_angle_to_rot(y1, theta) @ x1
y2 = axis_angle_to_rot(y1, theta) @ y1
z2 = axis_angle_to_rot(y1, theta) @ z1
x3 = axis_angle_to_rot(x2, phi) @ x2
y3 = axis_angle_to_rot(x2, phi) @ y2
x0 = np.array([1.0, 0, 0])
y0 = np.array([0, 1.0, 0])
z0 = np.array([0, 0, 1.0])
psi_out = np.arctan2(np.dot(x3, y0), np.dot(x3, x0))
theta_out = np.arctan2(-np.dot(x3, z0), np.sqrt(np.dot(x3, x0)**2 + np.dot(x3, y0)**2))
y2 = axis_angle_to_rot(z0, psi_out) @ y0
z2 = axis_angle_to_rot(y2, theta_out) @ z0
phi_out = np.arctan2(np.dot(y3, z2), np.dot(y3, y2))
return np.array([phi_out, theta_out, psi_out])
def ned_euler_from_ecef_single(ecef_init, ecef_pose):
"""
Convert ECEF Euler angles (roll, pitch, yaw) at a given ECEF origin
to equivalent NED Euler angles.
"""
converter = LocalCoord(ecef=ecef_init)
x0 = np.array([1.0, 0, 0])
y0 = np.array([0, 1.0, 0])
z0 = np.array([0, 0, 1.0])
phi, theta, psi = ecef_pose
x1 = axis_angle_to_rot(z0, psi) @ x0
y1 = axis_angle_to_rot(z0, psi) @ y0
z1 = axis_angle_to_rot(z0, psi) @ z0
x2 = axis_angle_to_rot(y1, theta) @ x1
y2 = axis_angle_to_rot(y1, theta) @ y1
z2 = axis_angle_to_rot(y1, theta) @ z1
x3 = axis_angle_to_rot(x2, phi) @ x2
y3 = axis_angle_to_rot(x2, phi) @ y2
zero = np.array(ecef_init)
x0 = converter.ned2ecef_single([1, 0, 0]) - zero
y0 = converter.ned2ecef_single([0, 1, 0]) - zero
z0 = converter.ned2ecef_single([0, 0, 1]) - zero
psi_out = np.arctan2(np.dot(x3, y0), np.dot(x3, x0))
theta_out = np.arctan2(-np.dot(x3, z0), np.sqrt(np.dot(x3, x0)**2 + np.dot(x3, y0)**2))
y2 = axis_angle_to_rot(z0, psi_out) @ y0
z2 = axis_angle_to_rot(y2, theta_out) @ z0
phi_out = np.arctan2(np.dot(y3, z2), np.dot(y3, y2))
return np.array([phi_out, theta_out, psi_out])
-173
View File
@@ -1,173 +0,0 @@
# distutils: language = c++
# cython: language_level = 3
from openpilot.common.transformations.transformations cimport Matrix3, Vector3, Quaternion
from openpilot.common.transformations.transformations cimport ECEF, NED, Geodetic
from openpilot.common.transformations.transformations cimport euler2quat as euler2quat_c
from openpilot.common.transformations.transformations cimport quat2euler as quat2euler_c
from openpilot.common.transformations.transformations cimport quat2rot as quat2rot_c
from openpilot.common.transformations.transformations cimport rot2quat as rot2quat_c
from openpilot.common.transformations.transformations cimport euler2rot as euler2rot_c
from openpilot.common.transformations.transformations cimport rot2euler as rot2euler_c
from openpilot.common.transformations.transformations cimport rot_matrix as rot_matrix_c
from openpilot.common.transformations.transformations cimport ecef_euler_from_ned as ecef_euler_from_ned_c
from openpilot.common.transformations.transformations cimport ned_euler_from_ecef as ned_euler_from_ecef_c
from openpilot.common.transformations.transformations cimport geodetic2ecef as geodetic2ecef_c
from openpilot.common.transformations.transformations cimport ecef2geodetic as ecef2geodetic_c
from openpilot.common.transformations.transformations cimport LocalCoord_c
import numpy as np
cimport numpy as np
cdef np.ndarray[double, ndim=2] matrix2numpy(Matrix3 m):
return np.array([
[m(0, 0), m(0, 1), m(0, 2)],
[m(1, 0), m(1, 1), m(1, 2)],
[m(2, 0), m(2, 1), m(2, 2)],
])
cdef Matrix3 numpy2matrix(np.ndarray[double, ndim=2, mode="fortran"] m):
assert m.shape[0] == 3
assert m.shape[1] == 3
return Matrix3(<double*>m.data)
cdef ECEF list2ecef(ecef):
cdef ECEF e
e.x = ecef[0]
e.y = ecef[1]
e.z = ecef[2]
return e
cdef NED list2ned(ned):
cdef NED n
n.n = ned[0]
n.e = ned[1]
n.d = ned[2]
return n
cdef Geodetic list2geodetic(geodetic):
cdef Geodetic g
g.lat = geodetic[0]
g.lon = geodetic[1]
g.alt = geodetic[2]
return g
def euler2quat_single(euler):
cdef Vector3 e = Vector3(euler[0], euler[1], euler[2])
cdef Quaternion q = euler2quat_c(e)
return [q.w(), q.x(), q.y(), q.z()]
def quat2euler_single(quat):
cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3])
cdef Vector3 e = quat2euler_c(q)
return [e(0), e(1), e(2)]
def quat2rot_single(quat):
cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3])
cdef Matrix3 r = quat2rot_c(q)
return matrix2numpy(r)
def rot2quat_single(rot):
cdef Matrix3 r = numpy2matrix(np.asfortranarray(rot, dtype=np.double))
cdef Quaternion q = rot2quat_c(r)
return [q.w(), q.x(), q.y(), q.z()]
def euler2rot_single(euler):
cdef Vector3 e = Vector3(euler[0], euler[1], euler[2])
cdef Matrix3 r = euler2rot_c(e)
return matrix2numpy(r)
def rot2euler_single(rot):
cdef Matrix3 r = numpy2matrix(np.asfortranarray(rot, dtype=np.double))
cdef Vector3 e = rot2euler_c(r)
return [e(0), e(1), e(2)]
def rot_matrix(roll, pitch, yaw):
return matrix2numpy(rot_matrix_c(roll, pitch, yaw))
def ecef_euler_from_ned_single(ecef_init, ned_pose):
cdef ECEF init = list2ecef(ecef_init)
cdef Vector3 pose = Vector3(ned_pose[0], ned_pose[1], ned_pose[2])
cdef Vector3 e = ecef_euler_from_ned_c(init, pose)
return [e(0), e(1), e(2)]
def ned_euler_from_ecef_single(ecef_init, ecef_pose):
cdef ECEF init = list2ecef(ecef_init)
cdef Vector3 pose = Vector3(ecef_pose[0], ecef_pose[1], ecef_pose[2])
cdef Vector3 e = ned_euler_from_ecef_c(init, pose)
return [e(0), e(1), e(2)]
def geodetic2ecef_single(geodetic):
cdef Geodetic g = list2geodetic(geodetic)
cdef ECEF e = geodetic2ecef_c(g)
return [e.x, e.y, e.z]
def ecef2geodetic_single(ecef):
cdef ECEF e = list2ecef(ecef)
cdef Geodetic g = ecef2geodetic_c(e)
return [g.lat, g.lon, g.alt]
cdef class LocalCoord:
cdef LocalCoord_c * lc
def __init__(self, geodetic=None, ecef=None):
assert (geodetic is not None) or (ecef is not None)
if geodetic is not None:
self.lc = new LocalCoord_c(list2geodetic(geodetic))
elif ecef is not None:
self.lc = new LocalCoord_c(list2ecef(ecef))
@property
def ned2ecef_matrix(self):
return matrix2numpy(self.lc.ned2ecef_matrix)
@property
def ecef2ned_matrix(self):
return matrix2numpy(self.lc.ecef2ned_matrix)
@property
def ned_from_ecef_matrix(self):
return self.ecef2ned_matrix
@property
def ecef_from_ned_matrix(self):
return self.ned2ecef_matrix
@classmethod
def from_geodetic(cls, geodetic):
return cls(geodetic=geodetic)
@classmethod
def from_ecef(cls, ecef):
return cls(ecef=ecef)
def ecef2ned_single(self, ecef):
assert self.lc
cdef ECEF e = list2ecef(ecef)
cdef NED n = self.lc.ecef2ned(e)
return [n.n, n.e, n.d]
def ned2ecef_single(self, ned):
assert self.lc
cdef NED n = list2ned(ned)
cdef ECEF e = self.lc.ned2ecef(n)
return [e.x, e.y, e.z]
def geodetic2ned_single(self, geodetic):
assert self.lc
cdef Geodetic g = list2geodetic(geodetic)
cdef NED n = self.lc.geodetic2ned(g)
return [n.n, n.e, n.d]
def ned2geodetic_single(self, ned):
assert self.lc
cdef NED n = list2ned(ned)
cdef Geodetic g = self.lc.ned2geodetic(n)
return [g.lat, g.lon, g.alt]
def __dealloc__(self):
del self.lc
-46
View File
@@ -1,46 +0,0 @@
import os
import subprocess
def sudo_write(val: str, path: str) -> None:
try:
with open(path, 'w') as f:
f.write(str(val))
except PermissionError:
os.system(f"sudo chmod a+w {path}")
try:
with open(path, 'w') as f:
f.write(str(val))
except PermissionError:
# fallback for debugfs files
os.system(f"sudo su -c 'echo {val} > {path}'")
def sudo_read(path: str) -> str:
try:
return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip()
except Exception:
return ""
class MovingAverage:
def __init__(self, window_size: int):
self.window_size: int = window_size
self.buffer: list[float] = [0.0] * window_size
self.index: int = 0
self.count: int = 0
self.sum: float = 0.0
def add_value(self, new_value: float):
# Update the sum: subtract the value being replaced and add the new value
self.sum -= self.buffer[self.index]
self.buffer[self.index] = new_value
self.sum += new_value
# Update the index in a circular manner
self.index = (self.index + 1) % self.window_size
# Track the number of added values (for partial windows)
self.count = min(self.count + 1, self.window_size)
def get_average(self) -> float:
if self.count == 0:
return float('nan')
return self.sum / self.count
+50 -3
View File
@@ -7,14 +7,61 @@ import time
import functools import functools
from subprocess import Popen, PIPE, TimeoutExpired from subprocess import Popen, PIPE, TimeoutExpired
import zstandard as zstd import zstandard as zstd
from openpilot.common.swaglog import cloudlog
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
def sudo_write(val: str, path: str) -> None:
try:
with open(path, 'w') as f:
f.write(str(val))
except PermissionError:
os.system(f"sudo chmod a+w {path}")
try:
with open(path, 'w') as f:
f.write(str(val))
except PermissionError:
# fallback for debugfs files
os.system(f"sudo su -c 'echo {val} > {path}'")
def sudo_read(path: str) -> str:
try:
return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip()
except Exception:
return ""
class MovingAverage:
def __init__(self, window_size: int):
self.window_size: int = window_size
self.buffer: list[float] = [0.0] * window_size
self.index: int = 0
self.count: int = 0
self.sum: float = 0.0
def add_value(self, new_value: float):
# Update the sum: subtract the value being replaced and add the new value
self.sum -= self.buffer[self.index]
self.buffer[self.index] = new_value
self.sum += new_value
# Update the index in a circular manner
self.index = (self.index + 1) % self.window_size
# Track the number of added values (for partial windows)
self.count = min(self.count + 1, self.window_size)
def get_average(self) -> float:
if self.count == 0:
return float('nan')
return self.sum / self.count
class CallbackReader: class CallbackReader:
"""Wraps a file, but overrides the read method to also """Wraps a file, but overrides the read method to also
call a callback function with the number of bytes read so far.""" call a callback function with the number of bytes read so far."""
def __init__(self, f, callback, *args): def __init__(self, f, callback, *args):
self.f = f self.f = f
self.callback = callback self.callback = callback
@@ -107,11 +154,11 @@ def retry(attempts=3, delay=1.0, ignore_failure=False):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except Exception: except Exception:
cloudlog.exception(f"{func.__name__} failed, trying again") print(f"{func.__name__} failed, trying again")
time.sleep(delay) time.sleep(delay)
if ignore_failure: if ignore_failure:
cloudlog.error(f"{func.__name__} failed after retry") print(f"{func.__name__} failed after retry")
else: else:
raise Exception(f"{func.__name__} failed after retry") raise Exception(f"{func.__name__} failed after retry")
return wrapper return wrapper
+1 -1
View File
@@ -1 +1 @@
#define COMMA_VERSION "0.10.3" #define COMMA_VERSION "0.10.4"
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12 export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="15.1" export AGNOS_VERSION="16"
fi fi
export STAGING_ROOT="/data/safe_staging" export STAGING_ROOT="/data/safe_staging"
+1 -1
Submodule panda updated: 5f3c09c910...f9cdec7f7b
+45 -43
View File
@@ -14,7 +14,7 @@ dependencies = [
"pyserial", # pigeond + qcomgpsd "pyserial", # pigeond + qcomgpsd
"requests", # many one-off uses "requests", # many one-off uses
"sympy", # rednose + friends "sympy", # rednose + friends
"crcmod", # cars + qcomgpsd "crcmod-plus", # cars + qcomgpsd
"tqdm", # cars (fw_versions.py) on start + many one-off uses "tqdm", # cars (fw_versions.py) on start + many one-off uses
# hardwared # hardwared
@@ -49,7 +49,7 @@ dependencies = [
# logging # logging
"pyzmq", "pyzmq",
"sentry-sdk", "sentry-sdk",
"xattr", # used in place of 'os.getxattr' for macos compatibility "xattr", # used in place of 'os.getxattr' for macOS compatibility
# athena # athena
"PyJWT", "PyJWT",
@@ -72,7 +72,7 @@ dependencies = [
"zstandard", "zstandard",
# ui # ui
"raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186 "raylib > 5.5.0.3",
"qrcode", "qrcode",
"mapbox-earcut", "mapbox-earcut",
] ]
@@ -87,7 +87,7 @@ docs = [
testing = [ testing = [
"coverage", "coverage",
"hypothesis ==6.47.*", "hypothesis ==6.47.*",
"mypy", "ty",
"pytest", "pytest",
"pytest-cpp", "pytest-cpp",
"pytest-subtests", "pytest-subtests",
@@ -107,15 +107,13 @@ dev = [
"av", "av",
"azure-identity", "azure-identity",
"azure-storage-blob", "azure-storage-blob",
"dbus-next", # TODO: remove once we moved everything to jeepney
"dictdiffer", "dictdiffer",
"jeepney", "jeepney",
"matplotlib", "matplotlib",
"opencv-python-headless", "opencv-python-headless",
"parameterized >=0.8, <0.9", "parameterized >=0.8, <0.9",
"pyautogui", "pyautogui",
"pygame", "pyopencl",
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
"pytools>=2025.1.6; platform_machine != 'aarch64'", "pytools>=2025.1.6; platform_machine != 'aarch64'",
"pywinctl", "pywinctl",
"pyprof2calltree", "pyprof2calltree",
@@ -181,42 +179,6 @@ ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,w
builtin = "clear,rare,informal,code,names,en-GB_to_en-US" builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html" skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
[tool.mypy]
python_version = "3.11"
exclude = [
"cereal/",
"msgq/",
"msgq_repo/",
"opendbc/",
"opendbc_repo/",
"panda/",
"rednose/",
"rednose_repo/",
"tinygrad/",
"tinygrad_repo/",
"teleoprtc/",
"teleoprtc_repo/",
"third_party/",
]
# third-party packages
ignore_missing_imports=true
# helpful warnings
warn_redundant_casts=true
warn_unreachable=true
warn_unused_ignores=true
# restrict dynamic typing
warn_return_any=true
# allow implicit optionals for default args
implicit_optional = true
local_partial_types=true
explicit_package_bases=true
disable_error_code = "annotation-unchecked"
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml # https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
[tool.ruff] [tool.ruff]
indent-width = 2 indent-width = 2
@@ -275,3 +237,43 @@ lint.flake8-implicit-str-concat.allow-multiline = false
[tool.ruff.format] [tool.ruff.format]
quote-style = "preserve" quote-style = "preserve"
[tool.ty.src]
exclude = [
"cereal/",
"msgq/",
"msgq_repo/",
"opendbc/",
"opendbc_repo/",
"panda/",
"rednose/",
"rednose_repo/",
"tinygrad/",
"tinygrad_repo/",
"teleoprtc/",
"teleoprtc_repo/",
"third_party/",
]
[tool.ty.rules]
# Ignore unresolved imports for Cython-compiled modules (.pyx)
unresolved-import = "ignore"
# Ignore unresolved attributes - many from capnp and Cython modules
unresolved-attribute = "ignore"
# Ignore invalid method overrides - signature variance issues
invalid-method-override = "ignore"
# Ignore possibly-missing-attribute - too many false positives
possibly-missing-attribute = "ignore"
# Ignore invalid assignment - often intentional monkey-patching
invalid-assignment = "ignore"
# Ignore no-matching-overload - numpy/ctypes overload matching issues
no-matching-overload = "ignore"
# Ignore invalid-argument-type - many false positives from raylib, ctypes, numpy
invalid-argument-type = "ignore"
# Ignore call-non-callable - false positives from dynamic types
call-non-callable = "ignore"
# Ignore unsupported-operator - false positives from dynamic types
unsupported-operator = "ignore"
# Ignore not-subscriptable - false positives from dynamic types
not-subscriptable = "ignore"
# not-iterable errors are now fixed
+8 -9
View File
@@ -4,18 +4,17 @@
## release checklist ## release checklist
### Go to staging ### Go to staging
- [ ] make a GitHub issue to track release - [ ] make a GitHub issue to track release with this checklist
- [ ] create release master branch - [ ] create release master branch
- [ ] update RELEASES.md - [ ] create a branch from upstream master named `zerotentwo` for release `v0.10.2`
- [ ] revert risky commits (double check with autonomy team)
- [ ] push the new branch
- [ ] push to staging:
- [ ] make sure you are on the newly created release master branch (`zerotentwo`)
- [ ] run `BRANCH=devel-staging release/build_stripped.sh`. Jenkins will then automatically build staging on device, run `test_onroad` and update the staging branch
- [ ] bump version on master: `common/version.h` and `RELEASES.md` - [ ] bump version on master: `common/version.h` and `RELEASES.md`
- [ ] build new userdata partition from `release3-staging`
- [ ] post on Discord, tag `@release crew` - [ ] post on Discord, tag `@release crew`
Updating staging:
1. either rebase on master or cherry-pick changes
2. run this to update: `BRANCH=devel-staging release/build_devel.sh`
3. build new userdata partition from `release3-staging`
### Go to release ### Go to release
- [ ] before going to release, test the following: - [ ] before going to release, test the following:
- [ ] update from previous release -> new release - [ ] update from previous release -> new release
@@ -26,7 +25,7 @@ Updating staging:
- [ ] check sentry, MTBF, etc. - [ ] check sentry, MTBF, etc.
- [ ] stress test passes in production - [ ] stress test passes in production
- [ ] publish the blog post - [ ] publish the blog post
- [ ] `git reset --hard origin/release3-staging` - [ ] `git reset --hard origin/release-mici-staging`
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X` - [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
- [ ] create GitHub release - [ ] create GitHub release
- [ ] final test install on `openpilot.comma.ai` - [ ] final test install on `openpilot.comma.ai`
+6 -6
View File
@@ -55,7 +55,7 @@ function run_tests() {
run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES
if [[ -z "$FAST" ]]; then if [[ -z "$FAST" ]]; then
run "mypy" mypy $PYTHON_FILES run "ty" ty check
run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore
fi fi
@@ -69,7 +69,7 @@ function help() {
echo "" echo ""
echo -e "${BOLD}${UNDERLINE}Tests:${NC}" echo -e "${BOLD}${UNDERLINE}Tests:${NC}"
echo -e " ${BOLD}ruff${NC}" echo -e " ${BOLD}ruff${NC}"
echo -e " ${BOLD}mypy${NC}" echo -e " ${BOLD}ty${NC}"
echo -e " ${BOLD}codespell${NC}" echo -e " ${BOLD}codespell${NC}"
echo -e " ${BOLD}check_added_large_files${NC}" echo -e " ${BOLD}check_added_large_files${NC}"
echo -e " ${BOLD}check_shebang_scripts_are_executable${NC}" echo -e " ${BOLD}check_shebang_scripts_are_executable${NC}"
@@ -81,11 +81,11 @@ function help() {
echo " Specify tests to skip separated by spaces" echo " Specify tests to skip separated by spaces"
echo "" echo ""
echo -e "${BOLD}${UNDERLINE}Examples:${NC}" echo -e "${BOLD}${UNDERLINE}Examples:${NC}"
echo " op lint mypy ruff" echo " op lint ty ruff"
echo " Only run the mypy and ruff tests" echo " Only run the ty and ruff tests"
echo "" echo ""
echo " op lint --skip mypy ruff" echo " op lint --skip ty ruff"
echo " Skip the mypy and ruff tests" echo " Skip the ty and ruff tests"
echo "" echo ""
echo " op lint" echo " op lint"
echo " Run all the tests" echo " Run all the tests"
+19 -54
View File
@@ -1,7 +1,8 @@
from cereal import car, log, custom from cereal import car, log
import cereal.messaging as messaging
from opendbc.car import DT_CTRL, structs from opendbc.car import DT_CTRL, structs
from opendbc.car.car_helpers import interfaces
from opendbc.car.interfaces import MAX_CTRL_SPEED from opendbc.car.interfaces import MAX_CTRL_SPEED
from opendbc.car.toyota.values import ToyotaFlags
from openpilot.selfdrive.selfdrived.events import Events from openpilot.selfdrive.selfdrived.events import Events
@@ -11,33 +12,6 @@ EventName = log.OnroadEvent.EventName
NetworkLocation = structs.CarParams.NetworkLocation NetworkLocation = structs.CarParams.NetworkLocation
# TODO: the goal is to abstract this file into the CarState struct and make events generic
class MockCarState:
def __init__(self):
self.sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal'])
def update(self, CS: car.CarState, CS_SP: custom.CarStateSP):
self.sm.update(0)
gps_sock = 'gpsLocationExternal' if self.sm.recv_frame['gpsLocationExternal'] > 1 else 'gpsLocation'
CS.vEgo = self.sm[gps_sock].speed
CS.vEgoRaw = self.sm[gps_sock].speed
return CS, CS_SP
BRAND_EXTRA_GEARS = {
'ford': [GearShifter.low, GearShifter.manumatic],
'nissan': [GearShifter.brake],
'chrysler': [GearShifter.low],
'honda': [GearShifter.sport],
'toyota': [GearShifter.sport],
'gm': [GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic],
'volkswagen': [GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
'hyundai': [GearShifter.sport, GearShifter.manumatic]
}
class CarSpecificEvents: class CarSpecificEvents:
def __init__(self, CP: structs.CarParams): def __init__(self, CP: structs.CarParams):
self.CP = CP self.CP = CP
@@ -48,14 +22,12 @@ class CarSpecificEvents:
self.silent_steer_warning = True self.silent_steer_warning = True
def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl): def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl):
extra_gears = BRAND_EXTRA_GEARS.get(self.CP.brand, None)
if self.CP.brand in ('body', 'mock'): if self.CP.brand in ('body', 'mock'):
events = Events() return Events()
elif self.CP.brand == 'chrysler': events = self.create_common_events(CS, CS_prev)
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
if self.CP.brand == 'chrysler':
# Low speed steer alert hysteresis logic # Low speed steer alert hysteresis logic
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5): if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
self.low_speed_alert = True self.low_speed_alert = True
@@ -65,8 +37,6 @@ class CarSpecificEvents:
events.add(EventName.belowSteerSpeed) events.add(EventName.belowSteerSpeed)
elif self.CP.brand == 'honda': elif self.CP.brand == 'honda':
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=False)
if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed: if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed:
events.add(EventName.belowEngageSpeed) events.add(EventName.belowEngageSpeed)
@@ -87,11 +57,9 @@ class CarSpecificEvents:
elif self.CP.brand == 'toyota': elif self.CP.brand == 'toyota':
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3 # TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
if self.CP.openpilotLongitudinalControl: if self.CP.openpilotLongitudinalControl:
# Only can leave standstill when planner wants to move # Only can leave standstill when planner wants to move
if CS.cruiseState.standstill and not CS.brakePressed and CC.cruiseControl.resume: if CS.cruiseState.standstill and not CS.brakePressed and (CC.cruiseControl.resume or self.CP.flags & ToyotaFlags.HYBRID.value):
events.add(EventName.resumeRequired) events.add(EventName.resumeRequired)
if CS.vEgo < self.CP.minEnableSpeed: if CS.vEgo < self.CP.minEnableSpeed:
events.add(EventName.belowEngageSpeed) events.add(EventName.belowEngageSpeed)
@@ -103,8 +71,6 @@ class CarSpecificEvents:
events.add(EventName.manualRestart) events.add(EventName.manualRestart)
elif self.CP.brand == 'gm': elif self.CP.brand == 'gm':
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
# Enabling at a standstill with brake is allowed # Enabling at a standstill with brake is allowed
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs # TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
if CS.vEgo < self.CP.minEnableSpeed and not (CS.standstill and CS.brake >= 20 and if CS.vEgo < self.CP.minEnableSpeed and not (CS.standstill and CS.brake >= 20 and
@@ -114,8 +80,6 @@ class CarSpecificEvents:
events.add(EventName.resumeRequired) events.add(EventName.resumeRequired)
elif self.CP.brand == 'volkswagen': elif self.CP.brand == 'volkswagen':
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
if self.CP.openpilotLongitudinalControl: if self.CP.openpilotLongitudinalControl:
if CS.vEgo < self.CP.minEnableSpeed + 0.5: if CS.vEgo < self.CP.minEnableSpeed + 0.5:
events.add(EventName.belowEngageSpeed) events.add(EventName.belowEngageSpeed)
@@ -123,27 +87,26 @@ class CarSpecificEvents:
events.add(EventName.speedTooLow) events.add(EventName.speedTooLow)
# TODO: this needs to be implemented generically in carState struct # TODO: this needs to be implemented generically in carState struct
# if CC.eps_timer_soft_disable_alert: # type: ignore[attr-defined] # if CC.eps_timer_soft_disable_alert:
# events.add(EventName.steerTimeLimit) # events.add(EventName.steerTimeLimit)
elif self.CP.brand == 'hyundai':
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
else:
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
return events return events
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True, def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState):
allow_button_cancel=True):
events = Events() events = Events()
CI = interfaces[self.CP.carFingerprint]
# TODO: cleanup the honda-specific logic
pcm_enable = self.CP.pcmCruise and self.CP.brand != 'honda'
# TODO: on some hyundai cars, the cancel button is also the pause/resume button,
# so only use it for cancel when running openpilot longitudinal
allow_button_cancel = self.CP.brand != 'hyundai'
if CS.doorOpen: if CS.doorOpen:
events.add(EventName.doorOpen) events.add(EventName.doorOpen)
if CS.seatbeltUnlatched: if CS.seatbeltUnlatched:
events.add(EventName.seatbeltNotLatched) events.add(EventName.seatbeltNotLatched)
if CS.gearShifter != GearShifter.drive and (extra_gears is None or if CS.gearShifter != GearShifter.drive and CS.gearShifter not in CI.DRIVABLE_GEARS:
CS.gearShifter not in extra_gears):
events.add(EventName.wrongGear) events.add(EventName.wrongGear)
if CS.gearShifter == GearShifter.reverse: if CS.gearShifter == GearShifter.reverse:
events.add(EventName.reverseGear) events.add(EventName.reverseGear)
@@ -157,6 +120,8 @@ class CarSpecificEvents:
events.add(EventName.stockFcw) events.add(EventName.stockFcw)
if CS.stockAeb: if CS.stockAeb:
events.add(EventName.stockAeb) events.add(EventName.stockAeb)
if CS.stockLkas:
events.add(EventName.stockLkas)
if CS.vEgo > MAX_CTRL_SPEED: if CS.vEgo > MAX_CTRL_SPEED:
events.add(EventName.speedTooHigh) events.add(EventName.speedTooHigh)
if CS.cruiseState.nonAdaptive: if CS.cruiseState.nonAdaptive:
+1 -5
View File
@@ -19,7 +19,6 @@ from opendbc.car.car_helpers import get_car, interfaces
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
from openpilot.selfdrive.car.cruise import VCruiseHelper from openpilot.selfdrive.car.cruise import VCruiseHelper
from openpilot.selfdrive.car.car_specific import MockCarState
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
@@ -139,7 +138,7 @@ class Car:
safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput
self.CP.safetyConfigs = [safety_config] self.CP.safetyConfigs = [safety_config]
if self.CP.secOcRequired and not is_release: if self.CP.secOcRequired:
# Copy user key if available # Copy user key if available
try: try:
with open("/cache/params/SecOCKey") as f: with open("/cache/params/SecOCKey") as f:
@@ -179,7 +178,6 @@ class Car:
self.params.put_nonblocking("CarParamsSPCache", cp_sp_bytes) self.params.put_nonblocking("CarParamsSPCache", cp_sp_bytes)
self.params.put_nonblocking("CarParamsSPPersistent", cp_sp_bytes) self.params.put_nonblocking("CarParamsSPPersistent", cp_sp_bytes)
self.mock_carstate = MockCarState()
self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP) self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP)
self.is_metric = self.params.get_bool("IsMetric") self.is_metric = self.params.get_bool("IsMetric")
@@ -200,8 +198,6 @@ class Car:
# Update carState from CAN # Update carState from CAN
CS, CS_SP = self.CI.update(can_list) CS, CS_SP = self.CI.update(can_list)
CS_SP = convert_to_capnp(CS_SP) CS_SP = convert_to_capnp(CS_SP)
if self.CP.brand == 'mock':
CS, CS_SP = self.mock_carstate.update(CS, CS_SP)
# Update radar tracks from CAN # Update radar tracks from CAN
RD: structs.RadarDataT | None = self.RI.update(can_list) RD: structs.RadarDataT | None = self.RI.update(can_list)
+2 -2
View File
@@ -3,7 +3,7 @@ import numpy as np
from collections import deque from collections import deque
from cereal import log from cereal import log
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from opendbc.car.lateral import get_friction, get_friction_threshold
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol import LatControl
@@ -95,7 +95,7 @@ class LatControlTorque(LatControl):
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
ff -= self.torque_params.latAccelOffset ff -= self.torque_params.latAccelOffset
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it # TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) ff += get_friction(error, lateral_accel_deadzone, get_friction_threshold(CS.vEgo), self.torque_params)
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_lataccel = self.pid.update(pid_log.error, output_lataccel = self.pid.update(pid_log.error,
@@ -35,15 +35,14 @@ X_EGO_OBSTACLE_COST = 3.
X_EGO_COST = 0. X_EGO_COST = 0.
V_EGO_COST = 0. V_EGO_COST = 0.
A_EGO_COST = 0. A_EGO_COST = 0.
J_EGO_COST = 5.0 J_EGO_COST = 10.0
A_CHANGE_COST = 200. A_CHANGE_COST = 150.
DANGER_ZONE_COST = 100. DANGER_ZONE_COST = 100.
CRASH_DISTANCE = .25 CRASH_DISTANCE = .25
LEAD_DANGER_FACTOR = 0.75 LEAD_DANGER_FACTOR = 0.75
LIMIT_COST = 1e6 LIMIT_COST = 1e6
ACADOS_SOLVER_TYPE = 'SQP_RTI' ACADOS_SOLVER_TYPE = 'SQP_RTI'
# Fewer timestamps don't hurt performance and lead to # Fewer timestamps don't hurt performance and lead to
# much better convergence of the MPC with low iterations # much better convergence of the MPC with low iterations
N = 12 N = 12
@@ -53,7 +52,7 @@ T_IDXS_LST = [index_function(idx, max_val=MAX_T, max_idx=N) for idx in range(N+1
T_IDXS = np.array(T_IDXS_LST) T_IDXS = np.array(T_IDXS_LST)
FCW_IDXS = T_IDXS < 5.0 FCW_IDXS = T_IDXS < 5.0
T_DIFFS = np.diff(T_IDXS, prepend=[0.]) T_DIFFS = np.diff(T_IDXS, prepend=[0.])
COMFORT_BRAKE = 2.5 COMFORT_BRAKE = 2.0
STOP_DISTANCE = 6.0 STOP_DISTANCE = 6.0
CRUISE_MIN_ACCEL = -1.2 CRUISE_MIN_ACCEL = -1.2
CRUISE_MAX_ACCEL = 1.6 CRUISE_MAX_ACCEL = 1.6
@@ -85,20 +84,12 @@ def get_stopped_equivalence_factor(v_lead):
def get_safe_obstacle_distance(v_ego, t_follow): def get_safe_obstacle_distance(v_ego, t_follow):
return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE
def desired_follow_distance(v_ego, v_lead, t_follow=None):
if t_follow is None:
t_follow = get_T_FOLLOW()
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
def gen_long_model(): def gen_long_model():
model = AcadosModel() model = AcadosModel()
model.name = MODEL_NAME model.name = MODEL_NAME
# set up states & controls # states
x_ego = SX.sym('x_ego') x_ego, v_ego, a_ego = SX.sym('x_ego'), SX.sym('v_ego'), SX.sym('a_ego')
v_ego = SX.sym('v_ego')
a_ego = SX.sym('a_ego')
model.x = vertcat(x_ego, v_ego, a_ego) model.x = vertcat(x_ego, v_ego, a_ego)
# controls # controls
@@ -126,7 +117,6 @@ def gen_long_model():
model.f_expl_expr = f_expl model.f_expl_expr = f_expl
return model return model
def gen_long_ocp(): def gen_long_ocp():
ocp = AcadosOcp() ocp = AcadosOcp()
ocp.model = gen_long_model() ocp.model = gen_long_model()
@@ -222,30 +212,31 @@ def gen_long_ocp():
class LongitudinalMpc: class LongitudinalMpc:
def __init__(self, mode='acc', dt=DT_MDL): def __init__(self, dt=DT_MDL):
self.mode = mode
self.dt = dt self.dt = dt
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.reset() self.reset()
self.source = SOURCES[2] self.source = SOURCES[2]
def reset(self): def reset(self):
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.solver.reset() self.solver.reset()
# self.solver.options_set('print_level', 2)
self.x_sol = np.zeros((N+1, X_DIM))
self.u_sol = np.zeros((N, 1))
self.v_solution = np.zeros(N+1) self.v_solution = np.zeros(N+1)
self.a_solution = np.zeros(N+1) self.a_solution = np.zeros(N+1)
self.prev_a = np.array(self.a_solution)
self.j_solution = np.zeros(N) self.j_solution = np.zeros(N)
self.prev_a = np.array(self.a_solution)
self.yref = np.zeros((N+1, COST_DIM)) self.yref = np.zeros((N+1, COST_DIM))
for i in range(N): for i in range(N):
self.solver.cost_set(i, "yref", self.yref[i]) self.solver.cost_set(i, "yref", self.yref[i])
self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM])
self.x_sol = np.zeros((N+1, X_DIM))
self.u_sol = np.zeros((N,1))
self.params = np.zeros((N+1, PARAM_DIM)) self.params = np.zeros((N+1, PARAM_DIM))
for i in range(N+1): for i in range(N+1):
self.solver.set(i, 'x', np.zeros(X_DIM)) self.solver.set(i, 'x', np.zeros(X_DIM))
self.last_cloudlog_t = 0 self.last_cloudlog_t = 0
self.status = False self.status = False
self.crash_cnt = 0.0 self.crash_cnt = 0.0
@@ -276,16 +267,9 @@ class LongitudinalMpc:
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard): def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
jerk_factor = get_jerk_factor(personality) jerk_factor = get_jerk_factor(personality)
if self.mode == 'acc': a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0 cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST]
cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST] constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
elif self.mode == 'blended':
a_change_cost = 40.0 if prev_accel_constraint else 0
cost_weights = [0., 0.1, 0.2, 5.0, a_change_cost, 1.0]
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
else:
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set')
self.set_cost_weights(cost_weights, constraint_cost_weights) self.set_cost_weights(cost_weights, constraint_cost_weights)
def set_cur_state(self, v, a): def set_cur_state(self, v, a):
@@ -320,14 +304,14 @@ class LongitudinalMpc:
# MPC will not converge if immediate crash is expected # MPC will not converge if immediate crash is expected
# Clip lead distance to what is still possible to brake for # Clip lead distance to what is still possible to brake for
min_x_lead = ((v_ego + v_lead)/2) * (v_ego - v_lead) / (-ACCEL_MIN * 2) min_x_lead = (v_ego + v_lead) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
x_lead = np.clip(x_lead, min_x_lead, 1e8) x_lead = np.clip(x_lead, min_x_lead, 1e8)
v_lead = np.clip(v_lead, 0.0, 1e8) v_lead = np.clip(v_lead, 0.0, 1e8)
a_lead = np.clip(a_lead, -10., 5.) a_lead = np.clip(a_lead, -10., 5.)
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau) lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
return lead_xv return lead_xv
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard): def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
t_follow = get_T_FOLLOW(personality) t_follow = get_T_FOLLOW(personality)
v_ego = self.x0[1] v_ego = self.x0[1]
self.status = radarstate.leadOne.status or radarstate.leadTwo.status self.status = radarstate.leadOne.status or radarstate.leadTwo.status
@@ -341,56 +325,28 @@ class LongitudinalMpc:
lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1]) lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1])
lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1]) lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1])
self.params[:,0] = ACCEL_MIN # Fake an obstacle for cruise, this ensures smooth acceleration to set speed
self.params[:,1] = ACCEL_MAX # when the leads are no factor.
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
# TODO does this make sense when max_a is negative?
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
# Update in ACC mode or ACC/e2e blend x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
if self.mode == 'acc': self.source = SOURCES[np.argmin(x_obstacles[0])]
self.params[:,5] = LEAD_DANGER_FACTOR
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed self.yref[:,:] = 0.0
# when the leads are no factor.
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
# TODO does this make sense when max_a is negative?
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
v_lower,
v_upper)
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
self.source = SOURCES[np.argmin(x_obstacles[0])]
# These are not used in ACC mode
x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0
elif self.mode == 'blended':
self.params[:,5] = 1.0
x_obstacles = np.column_stack([lead_0_obstacle,
lead_1_obstacle])
cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0]
xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1])
x = np.cumsum(np.insert(xforward, 0, x[0]))
x_and_cruise = np.column_stack([x, cruise_target])
x = np.min(x_and_cruise, axis=1)
self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise'
else:
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update')
self.yref[:,1] = x
self.yref[:,2] = v
self.yref[:,3] = a
self.yref[:,5] = j
for i in range(N): for i in range(N):
self.solver.set(i, "yref", self.yref[i]) self.solver.set(i, "yref", self.yref[i])
self.solver.set(N, "yref", self.yref[N][:COST_E_DIM]) self.solver.set(N, "yref", self.yref[N][:COST_E_DIM])
self.params[:,0] = ACCEL_MIN
self.params[:,1] = ACCEL_MAX
self.params[:,2] = np.min(x_obstacles, axis=1) self.params[:,2] = np.min(x_obstacles, axis=1)
self.params[:,3] = np.copy(self.prev_a) self.params[:,3] = np.copy(self.prev_a)
self.params[:,4] = t_follow self.params[:,4] = t_follow
self.params[:,5] = LEAD_DANGER_FACTOR
self.run() self.run()
if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and
@@ -399,18 +355,7 @@ class LongitudinalMpc:
else: else:
self.crash_cnt = 0 self.crash_cnt = 0
# Check if it got within lead comfort range
# TODO This should be done cleaner
if self.mode == 'blended':
if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0):
self.source = 'lead0'
if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \
(lead_1_obstacle[0] - lead_0_obstacle[0]):
self.source = 'lead1'
def run(self): def run(self):
# t0 = time.monotonic()
# reset = 0
for i in range(N+1): for i in range(N+1):
self.solver.set(i, 'p', self.params[i]) self.solver.set(i, 'p', self.params[i])
self.solver.constraints_set(0, "lbx", self.x0) self.solver.constraints_set(0, "lbx", self.x0)
@@ -422,13 +367,6 @@ class LongitudinalMpc:
self.time_linearization = float(self.solver.get_stats('time_lin')[0]) self.time_linearization = float(self.solver.get_stats('time_lin')[0])
self.time_integrator = float(self.solver.get_stats('time_sim')[0]) self.time_integrator = float(self.solver.get_stats('time_sim')[0])
# qp_iter = self.solver.get_stats('statistics')[-1][-1] # SQP_RTI specific
# print(f"long_mpc timings: tot {self.solve_time:.2e}, qp {self.time_qp_solution:.2e}, lin {self.time_linearization:.2e}, \
# integrator {self.time_integrator:.2e}, qp_iter {qp_iter}")
# res = self.solver.get_residuals()
# print(f"long_mpc residuals: {res[0]:.2e}, {res[1]:.2e}, {res[2]:.2e}, {res[3]:.2e}")
# self.solver.print_statistics()
for i in range(N+1): for i in range(N+1):
self.x_sol[i] = self.solver.get(i, 'x') self.x_sol[i] = self.solver.get(i, 'x')
for i in range(N): for i in range(N):
@@ -446,12 +384,8 @@ class LongitudinalMpc:
self.last_cloudlog_t = t self.last_cloudlog_t = t
cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}") cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}")
self.reset() self.reset()
# reset = 1
# print(f"long_mpc timings: total internal {self.solve_time:.2e}, external: {(time.monotonic() - t0):.2e} qp {self.time_qp_solution:.2e}, \
# lin {self.time_linearization:.2e} qp_iter {qp_iter}, reset {reset}")
if __name__ == "__main__": if __name__ == "__main__":
ocp = gen_long_ocp() ocp = gen_long_ocp()
AcadosOcpSolver.generate(ocp, json_file=JSON_FILE) AcadosOcpSolver.generate(ocp, json_file=JSON_FILE)
# AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True)
+11 -18
View File
@@ -9,7 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
@@ -28,14 +28,12 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5
_A_TOTAL_MAX_V = [1.7, 3.2] _A_TOTAL_MAX_V = [1.7, 3.2]
_A_TOTAL_MAX_BP = [20., 40.] _A_TOTAL_MAX_BP = [20., 40.]
def get_max_accel(v_ego): def get_max_accel(v_ego):
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS) return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
def get_coast_accel(pitch): def get_coast_accel(pitch):
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
def limit_accel_in_turns(v_ego, angle_steers, a_target, CP): def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
""" """
This function returns a limited long acceleration allowed, depending on the existing lateral acceleration This function returns a limited long acceleration allowed, depending on the existing lateral acceleration
@@ -70,7 +68,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.v_desired_trajectory = np.zeros(CONTROL_N) self.v_desired_trajectory = np.zeros(CONTROL_N)
self.a_desired_trajectory = np.zeros(CONTROL_N) self.a_desired_trajectory = np.zeros(CONTROL_N)
self.j_desired_trajectory = np.zeros(CONTROL_N) self.j_desired_trajectory = np.zeros(CONTROL_N)
self.solverExecutionTime = 0.0
@staticmethod @staticmethod
def parse_model(model_msg): def parse_model(model_msg):
@@ -123,12 +120,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
# No change cost when user is controlling the speed, or when standstill # No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].standstill) prev_accel_constraint = not (reset_state or sm['carState'].standstill)
if mode == 'acc': accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
else:
accel_clip = [ACCEL_MIN, ACCEL_MAX]
if reset_state: if reset_state:
self.v_desired_filter.x = v_ego self.v_desired_filter.x = v_ego
@@ -137,7 +131,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
# Prevent divergence, smooth in current v_ego # Prevent divergence, smooth in current v_ego
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
x, v, a, j, throttle_prob = self.parse_model(sm['modelV2']) _, _, _, _, throttle_prob = self.parse_model(sm['modelV2'])
# Don't clip at low speeds since throttle_prob doesn't account for creep # Don't clip at low speeds since throttle_prob doesn't account for creep
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
@@ -154,7 +148,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality) self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired) self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality) self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution) self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
@@ -176,12 +170,11 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop output_should_stop_e2e = sm['modelV2'].action.shouldStop
if mode == 'acc' or not self.mlsim: output_a_target = min(output_a_target_mpc, output_a_target_e2e)
output_a_target = output_a_target_mpc self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
self.output_should_stop = output_should_stop_mpc if output_a_target_e2e < output_a_target_mpc:
else: self.mpc.source = SOURCES[3]
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
for idx in range(2): for idx in range(2):
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
@@ -4,10 +4,15 @@ from parameterized import parameterized_class
from cereal import log from cereal import log
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
def desired_follow_distance(v_ego, v_lead, t_follow=None):
if t_follow is None:
t_follow = get_T_FOLLOW()
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0): def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
man = Maneuver( man = Maneuver(
'', '',
@@ -108,11 +108,11 @@ if __name__ == "__main__":
uds_client = UdsClient(panda, 0x7D0, bus=args.bus) uds_client = UdsClient(panda, 0x7D0, bus=args.bus)
print("\n[START DIAGNOSTIC SESSION]") print("\n[START DIAGNOSTIC SESSION]")
session_type : SESSION_TYPE = 0x07 # type: ignore session_type : SESSION_TYPE = 0x07
uds_client.diagnostic_session_control(session_type) uds_client.diagnostic_session_control(session_type)
print("[HARDWARE/SOFTWARE VERSION]") print("[HARDWARE/SOFTWARE VERSION]")
fw_version_data_id : DATA_IDENTIFIER_TYPE = 0xf100 # type: ignore fw_version_data_id : DATA_IDENTIFIER_TYPE = 0xf100
fw_version = uds_client.read_data_by_identifier(fw_version_data_id) fw_version = uds_client.read_data_by_identifier(fw_version_data_id)
print(fw_version) print(fw_version)
if fw_version not in SUPPORTED_FW_VERSIONS.keys(): if fw_version not in SUPPORTED_FW_VERSIONS.keys():
@@ -120,7 +120,7 @@ if __name__ == "__main__":
sys.exit(1) sys.exit(1)
print("[GET CONFIGURATION]") print("[GET CONFIGURATION]")
config_data_id : DATA_IDENTIFIER_TYPE = 0x0142 # type: ignore config_data_id : DATA_IDENTIFIER_TYPE = 0x0142
current_config = uds_client.read_data_by_identifier(config_data_id) current_config = uds_client.read_data_by_identifier(config_data_id)
config_values = SUPPORTED_FW_VERSIONS[fw_version] config_values = SUPPORTED_FW_VERSIONS[fw_version]
new_config = config_values.default_config if args.default else config_values.tracks_enabled new_config = config_values.default_config if args.default else config_values.tracks_enabled
+5 -5
View File
@@ -55,7 +55,7 @@ if __name__ == "__main__":
sw_ver = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER).decode("utf-8") sw_ver = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER).decode("utf-8")
component = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.SYSTEM_NAME_OR_ENGINE_TYPE).decode("utf-8") component = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.SYSTEM_NAME_OR_ENGINE_TYPE).decode("utf-8")
odx_file = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.ODX_FILE).decode("utf-8").rstrip('\x00') odx_file = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.ODX_FILE).decode("utf-8").rstrip('\x00')
current_coding = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING) # type: ignore current_coding = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING)
coding_text = current_coding.hex() coding_text = current_coding.hex()
print("\nEPS diagnostic data\n") print("\nEPS diagnostic data\n")
@@ -126,9 +126,9 @@ if __name__ == "__main__":
new_coding = current_coding[0:coding_byte] + new_byte.to_bytes(1, "little") + current_coding[coding_byte+1:] new_coding = current_coding[0:coding_byte] + new_byte.to_bytes(1, "little") + current_coding[coding_byte+1:]
try: try:
seed = uds_client.security_access(ACCESS_TYPE_LEVEL_1.REQUEST_SEED) # type: ignore seed = uds_client.security_access(ACCESS_TYPE_LEVEL_1.REQUEST_SEED)
key = struct.unpack("!I", seed)[0] + 28183 # yeah, it's like that key = struct.unpack("!I", seed)[0] + 28183 # yeah, it's like that
uds_client.security_access(ACCESS_TYPE_LEVEL_1.SEND_KEY, struct.pack("!I", key)) # type: ignore uds_client.security_access(ACCESS_TYPE_LEVEL_1.SEND_KEY, struct.pack("!I", key))
except (NegativeResponseError, MessageTimeoutError): except (NegativeResponseError, MessageTimeoutError):
print("Security access failed!") print("Security access failed!")
print("Open the hood and retry (disables the \"diagnostic firewall\" on newer vehicles)") print("Open the hood and retry (disables the \"diagnostic firewall\" on newer vehicles)")
@@ -148,7 +148,7 @@ if __name__ == "__main__":
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date) uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date)
tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER) tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER)
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER, tester_num) uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER, tester_num)
uds_client.write_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING, new_coding) # type: ignore uds_client.write_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING, new_coding)
except (NegativeResponseError, MessageTimeoutError): except (NegativeResponseError, MessageTimeoutError):
print("Writing new configuration failed!") print("Writing new configuration failed!")
print("Make sure the comma processes are stopped: tmux kill-session -t comma") print("Make sure the comma processes are stopped: tmux kill-session -t comma")
@@ -156,7 +156,7 @@ if __name__ == "__main__":
try: try:
# Read back result just to make 100% sure everything worked # Read back result just to make 100% sure everything worked
current_coding_text = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING).hex() # type: ignore current_coding_text = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING).hex()
print(f" New coding: {current_coding_text}") print(f" New coding: {current_coding_text}")
except (NegativeResponseError, MessageTimeoutError): except (NegativeResponseError, MessageTimeoutError):
print("Reading back updated coding failed!") print("Reading back updated coding failed!")
-1
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# type: ignore
''' '''
System tools like top/htop can only show current cpu usage values, so I write this script to do statistics jobs. System tools like top/htop can only show current cpu usage values, so I write this script to do statistics jobs.
Features: Features:
+2 -4
View File
@@ -99,8 +99,7 @@ def cycle_alerts(duration=200, is_metric=False):
alert = AM.process_alerts(frame, []) alert = AM.process_alerts(frame, [])
print(alert) print(alert)
for _ in range(duration): for _ in range(duration):
dat = messaging.new_message() dat = messaging.new_message('selfdriveState')
dat.init('selfdriveState')
dat.selfdriveState.enabled = False dat.selfdriveState.enabled = False
if alert: if alert:
@@ -112,8 +111,7 @@ def cycle_alerts(duration=200, is_metric=False):
dat.selfdriveState.alertSound = alert.audible_alert dat.selfdriveState.alertSound = alert.audible_alert
pm.send('selfdriveState', dat) pm.send('selfdriveState', dat)
dat = messaging.new_message() dat = messaging.new_message('deviceState')
dat.init('deviceState')
dat.deviceState.started = True dat.deviceState.started = True
pm.send('deviceState', dat) pm.send('deviceState', dat)
+6 -5
View File
@@ -28,11 +28,12 @@ def get_fingerprint(lr):
# TODO: also print the fw fingerprint merged with the existing ones # TODO: also print the fw fingerprint merged with the existing ones
# show FW fingerprint # show FW fingerprint
print("\nFW fingerprint:\n") if fw:
for f in fw: print("\nFW fingerprint:\n")
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [") for f in fw:
print(f" {f.fwVersion},") print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
print(" ],") print(f" {f.fwVersion},")
print(" ],")
print() print()
print(f"VIN: {vin}") print(f"VIN: {vin}")
-1
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# type: ignore
import random import random
from collections import defaultdict from collections import defaultdict
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# type: ignore
import os import os
import argparse import argparse
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# type: ignore
from collections import defaultdict from collections import defaultdict
import argparse import argparse
+2 -2
View File
@@ -47,7 +47,7 @@ DEBUG = os.getenv("DEBUG") is not None
def is_calibration_valid(rpy: np.ndarray) -> bool: def is_calibration_valid(rpy: np.ndarray) -> bool:
return (PITCH_LIMITS[0] < rpy[1] < PITCH_LIMITS[1]) and (YAW_LIMITS[0] < rpy[2] < YAW_LIMITS[1]) # type: ignore return (PITCH_LIMITS[0] < rpy[1] < PITCH_LIMITS[1]) and (YAW_LIMITS[0] < rpy[2] < YAW_LIMITS[1])
def sanity_clip(rpy: np.ndarray) -> np.ndarray: def sanity_clip(rpy: np.ndarray) -> np.ndarray:
@@ -92,7 +92,7 @@ class Calibrator:
valid_blocks: int = 0, valid_blocks: int = 0,
wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT, wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT,
height_init: np.ndarray = HEIGHT_INIT, height_init: np.ndarray = HEIGHT_INIT,
smooth_from: np.ndarray = None) -> None: smooth_from: np.ndarray | None = None) -> None:
if not np.isfinite(rpy_init).all(): if not np.isfinite(rpy_init).all():
self.rpy = RPY_INIT.copy() self.rpy = RPY_INIT.copy()
else: else:
+1 -1
View File
@@ -94,7 +94,7 @@ class PointBuckets:
def add_point(self, x: float, y: float) -> None: def add_point(self, x: float, y: float) -> None:
raise NotImplementedError raise NotImplementedError
def get_points(self, num_points: int = None) -> Any: def get_points(self, num_points: int | None = None) -> Any:
points = np.vstack([x.arr for x in self.buckets.values()]) points = np.vstack([x.arr for x in self.buckets.values()])
if num_points is None: if num_points is None:
return points return points
+2 -2
View File
@@ -127,8 +127,8 @@ class VehicleParamsLearner:
if not self.active: if not self.active:
# Reset time when stopped so uncertainty doesn't grow # Reset time when stopped so uncertainty doesn't grow
self.kf.filter.set_filter_time(t) # type: ignore self.kf.filter.set_filter_time(t)
self.kf.filter.reset_rewind() # type: ignore self.kf.filter.reset_rewind()
def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder: def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder:
x = self.kf.x x = self.kf.x
+1 -1
View File
@@ -35,7 +35,7 @@ MIN_BUCKET_POINTS = np.array([100, 300, 500, 500, 500, 500, 300, 100])
MIN_ENGAGE_BUFFER = 2 # secs MIN_ENGAGE_BUFFER = 2 # secs
VERSION = 1 # bump this to invalidate old parameter caches VERSION = 1 # bump this to invalidate old parameter caches
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda'] ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda', 'volkswagen']
def slope2rot(slope): def slope2rot(slope):
+1 -1
View File
@@ -1,7 +1,7 @@
import os import os
import glob import glob
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations') Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc')
lenv = env.Clone() lenv = env.Clone()
lenvCython = envCython.Clone() lenvCython = envCython.Clone()
+5 -1
View File
@@ -31,6 +31,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
@@ -43,7 +44,7 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl'
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl' VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl' POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
LAT_SMOOTH_SECONDS = 0.1 LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3 LONG_SMOOTH_SECONDS = 0.3
MIN_LAT_CONTROL_SPEED = 0.3 MIN_LAT_CONTROL_SPEED = 0.3
@@ -284,6 +285,7 @@ def main(demo=False):
buf_main, buf_extra = None, None buf_main, buf_extra = None, None
meta_main = FrameMeta() meta_main = FrameMeta()
meta_extra = FrameMeta() meta_extra = FrameMeta()
camera_offset_helper = CameraOffsetHelper()
if demo: if demo:
@@ -339,12 +341,14 @@ def main(demo=False):
v_ego = max(sm["carState"].vEgo, 0.) v_ego = max(sm["carState"].vEgo, 0.)
if sm.frame % 60 == 0: if sm.frame % 60 == 0:
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32) model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32)
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32) model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
live_calib_seen = True live_calib_seen = True
traffic_convention = np.zeros(2) traffic_convention = np.zeros(2)
+1 -1
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f8fe9a71b0fd428a045a82ed50790179f77aa664391198f078e11e7b2cb2c2d7 oid sha256:1edea5bb56f876db4cec97c150799513f6a59373f3ad152d55e4dcaab1b809e3
size 13926324 size 13926324
+1 -1
View File
@@ -14,7 +14,7 @@ with open(os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")) as
OFFROAD_ALERTS = json.load(f) OFFROAD_ALERTS = json.load(f)
def set_offroad_alert(alert: str, show_alert: bool, extra_text: str = None) -> None: def set_offroad_alert(alert: str, show_alert: bool, extra_text: str | None = None) -> None:
if show_alert: if show_alert:
a = copy.copy(OFFROAD_ALERTS[alert]) a = copy.copy(OFFROAD_ALERTS[alert])
a['extra'] = extra_text or '' a['extra'] = extra_text or ''
+12 -3
View File
@@ -303,6 +303,15 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"), ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"),
}, },
EventName.stockLkas: {
ET.PERMANENT: Alert(
"TAKE CONTROL",
"Stock LKAS: Lane Departure Detected",
AlertStatus.critical, AlertSize.full,
Priority.HIGH, VisualAlert.fcw, AudibleAlert.none, 2.),
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
},
EventName.fcw: { EventName.fcw: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"BRAKE!", "BRAKE!",
@@ -758,13 +767,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# - CAN data is received, but some message are not received at the right frequency # - CAN data is received, but some message are not received at the right frequency
# If you're not writing a new car port, this is usually cause by faulty wiring # If you're not writing a new car port, this is usually cause by faulty wiring
EventName.canError: { EventName.canError: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Error"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Unknown Vehicle Variant"),
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"CAN Error: Check Connections", "Unknown Vehicle Variant",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.),
ET.NO_ENTRY: NoEntryAlert("CAN Error: Check Connections"), ET.NO_ENTRY: NoEntryAlert("Unknown Vehicle Variant"),
}, },
EventName.canBusMissing: { EventName.canBusMissing: {
+1 -1
View File
@@ -44,7 +44,7 @@ class FuzzyGenerator:
except capnp.lib.capnp.KjException: except capnp.lib.capnp.KjException:
return self.generate_struct(field.schema) return self.generate_struct(field.schema)
def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str = None) -> st.SearchStrategy[dict[str, Any]]: def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str | None = None) -> st.SearchStrategy[dict[str, Any]]:
single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else () single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else ()
fields_to_generate = schema.non_union_fields + single_fill fields_to_generate = schema.non_union_fields + single_fill
return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate if not field.endswith('DEPRECATED')}) return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate if not field.endswith('DEPRECATED')})
+2 -1
View File
@@ -1,5 +1,6 @@
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
from typing import cast
import capnp import capnp
import functools import functools
import traceback import traceback
@@ -69,7 +70,7 @@ def migrate(lr: LogIterable, migration_funcs: list[MigrationFunc]):
if migration.product in grouped: # skip if product already exists if migration.product in grouped: # skip if product already exists
continue continue
sorted_indices = sorted(ii for i in migration.inputs for ii in grouped[i]) sorted_indices = sorted(ii for i in cast(list[str], migration.inputs) for ii in grouped.get(i, []))
msg_gen = [(i, lr[i]) for i in sorted_indices] msg_gen = [(i, lr[i]) for i in sorted_indices]
r_ops, a_ops, d_ops = migration(msg_gen) r_ops, a_ops, d_ops = migration(msg_gen)
replace_ops.extend(r_ops) replace_ops.extend(r_ops)
@@ -614,9 +614,9 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
def replay_process( def replay_process(
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None, cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None = None,
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None, fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None,
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False
) -> list[capnp._DynamicStructReader]: ) -> list[capnp._DynamicStructReader]:
if isinstance(cfg, Iterable): if isinstance(cfg, Iterable):
cfgs = list(cfg) cfgs = list(cfg)
+1 -1
View File
@@ -1 +1 @@
b508f43fb0481bce0859c9b6ab4f45ee690b8dab b259f6f8f099a9d82e4c65dd5deae2e4e293007b
+1 -1
View File
@@ -16,7 +16,7 @@ from openpilot.tools.lib.openpilotci import get_url
def regen_segment( def regen_segment(
lr: LogIterable, frs: dict[str, Any] = None, lr: LogIterable, frs: dict[str, Any] | None = None,
processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False
) -> list[capnp._DynamicStructReader]: ) -> list[capnp._DynamicStructReader]:
all_msgs = sorted(lr, key=lambda m: m.logMonoTime) all_msgs = sorted(lr, key=lambda m: m.logMonoTime)
+1 -1
View File
@@ -19,7 +19,7 @@ SOURCES: list[AzureContainer] = [
DEST = OpenpilotCIContainer DEST = OpenpilotCIContainer
def upload_route(path: str, exclude_patterns: Iterable[str] = None) -> None: def upload_route(path: str, exclude_patterns: Iterable[str] | None = None) -> None:
if exclude_patterns is None: if exclude_patterns is None:
exclude_patterns = [r'dcamera\.hevc'] exclude_patterns = [r'dcamera\.hevc']
+1 -1
View File
@@ -68,4 +68,4 @@ if GetOption('extras'):
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d) obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs) f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs)
# keep installers small # keep installers small
assert f[0].get_size() < 1900*1e3, f[0].get_size() assert f[0].get_size() < 19000*1e3, f[0].get_size()
+1 -1
View File
@@ -39,7 +39,7 @@ class HomeLayout(Widget):
self.current_state = HomeLayoutState.HOME self.current_state = HomeLayoutState.HOME
self.last_refresh = 0 self.last_refresh = 0
self.settings_callback: callable | None = None self.settings_callback: Callable[[], None] | None = None
self.update_available = False self.update_available = False
self.alert_count = 0 self.alert_count = 0
+1 -2
View File
@@ -1,4 +1,3 @@
from openpilot.common.params import Params
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -35,7 +34,7 @@ DESCRIPTIONS = {
class DeveloperLayout(Widget): class DeveloperLayout(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._params = Params() self._params = ui_state.params
self._is_release = self._params.get_bool("IsReleaseBranch") self._is_release = self._params.get_bool("IsReleaseBranch")
# Build items and keep references for callbacks/state updates # Build items and keep references for callbacks/state updates
+1 -2
View File
@@ -3,7 +3,6 @@ import math
from cereal import messaging, log from cereal import messaging, log
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
@@ -35,7 +34,7 @@ class DeviceLayout(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._params = Params() self._params = ui_state.params
self._select_language_dialog: MultiOptionDialog | None = None self._select_language_dialog: MultiOptionDialog | None = None
self._driver_camera: DriverCameraDialog | None = None self._driver_camera: DriverCameraDialog | None = None
self._pair_device_dialog: PairingDialog | None = None self._pair_device_dialog: PairingDialog | None = None
+3 -5
View File
@@ -145,20 +145,18 @@ class SettingsLayout(Widget):
if panel.instance: if panel.instance:
panel.instance.render(content_rect) panel.instance.render(content_rect)
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
# Check close button # Check close button
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback: if self._close_callback:
self._close_callback() self._close_callback()
return True return
# Check navigation buttons # Check navigation buttons
for panel_type, panel_info in self._panels.items(): for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect): if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
self.set_current_panel(panel_type) self.set_current_panel(panel_type)
return True return
return False
def set_current_panel(self, panel_type: PanelType): def set_current_panel(self, panel_type: PanelType):
if panel_type != self._current_panel: if panel_type != self._current_panel:
+2 -7
View File
@@ -1,5 +1,5 @@
from cereal import log from cereal import log
from openpilot.common.params import Params, UnknownKeyName from openpilot.common.params import UnknownKeyName
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets.scroller_tici import Scroller
@@ -41,7 +41,7 @@ DESCRIPTIONS = {
class TogglesLayout(Widget): class TogglesLayout(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._params = Params() self._params = ui_state.params
self._is_release = self._params.get_bool("IsReleaseBranch") self._is_release = self._params.get_bool("IsReleaseBranch")
# param, title, desc, icon, needs_restart # param, title, desc, icon, needs_restart
@@ -198,11 +198,6 @@ class TogglesLayout(Widget):
self._update_experimental_mode_icon() self._update_experimental_mode_icon()
# TODO: make a param control list item so we don't need to manage internal state as much here
# refresh toggles from params to mirror external changes
for param in self._toggle_defs:
self._toggles[param].action_item.set_state(self._params.get_bool(param))
# these toggles need restart, block while engaged # these toggles need restart, block while engaged
for toggle_def in self._toggle_defs: for toggle_def in self._toggle_defs:
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles: if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
+3 -1
View File
@@ -1,5 +1,6 @@
from enum import IntEnum from enum import IntEnum
import os import os
import requests
import threading import threading
import time import time
@@ -29,6 +30,7 @@ class PrimeState:
def __init__(self): def __init__(self):
self._params = Params() self._params = Params()
self._lock = threading.Lock() self._lock = threading.Lock()
self._session = requests.Session() # reuse session to reduce SSL handshake overhead
self.prime_type: PrimeType = self._load_initial_state() self.prime_type: PrimeType = self._load_initial_state()
self._running = False self._running = False
@@ -50,7 +52,7 @@ class PrimeState:
try: try:
identity_token = get_token(dongle_id) identity_token = get_token(dongle_id)
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token) response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token, session=self._session)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
is_paired = data.get("is_paired", False) is_paired = data.get("is_paired", False)
+15 -14
View File
@@ -169,8 +169,8 @@ class TrainingGuideDMTutorial(Widget):
def _update_state(self): def _update_state(self):
super()._update_state() super()._update_state()
if device.awake: if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
ui_state.params.put_bool("IsDriverViewEnabled", True) ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
sm = ui_state.sm sm = ui_state.sm
if sm.recv_frame.get("driverMonitoringState", 0) == 0: if sm.recv_frame.get("driverMonitoringState", 0) == 0:
@@ -240,19 +240,20 @@ class TrainingGuideDMTutorial(Widget):
ring_color, ring_color,
) )
self._back_button.render(rl.Rectangle( if self._dialog._camera_view.frame:
self._rect.x + 8, self._back_button.render(rl.Rectangle(
self._rect.y + self._rect.height - self._back_button.rect.height, self._rect.x + 8,
self._back_button.rect.width, self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.height, self._back_button.rect.width,
)) self._back_button.rect.height,
))
self._good_button.render(rl.Rectangle( self._good_button.render(rl.Rectangle(
self._rect.x + self._rect.width - self._good_button.rect.width - 8, self._rect.x + self._rect.width - self._good_button.rect.width - 8,
self._rect.y + self._rect.height - self._good_button.rect.height, self._rect.y + self._rect.height - self._good_button.rect.height,
self._good_button.rect.width, self._good_button.rect.width,
self._good_button.rect.height, self._good_button.rect.height,
)) ))
# rounded border # rounded border
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
@@ -1,3 +1,4 @@
import requests
import threading import threading
import time import time
import pyray as rl import pyray as rl
@@ -44,6 +45,7 @@ class FirehoseLayoutBase(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._params = Params() self._params = Params()
self._session = requests.Session() # reuse session to reduce SSL handshake overhead
self._segment_count = self._get_segment_count() self._segment_count = self._get_segment_count()
self._scroll_panel = GuiScrollPanel2(horizontal=False) self._scroll_panel = GuiScrollPanel2(horizontal=False)
@@ -203,7 +205,7 @@ class FirehoseLayoutBase(Widget):
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return return
identity_token = get_token(dongle_id) identity_token = get_token(dongle_id)
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token, session=self._session)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
self._segment_count = data.get("firehose", 0) self._segment_count = data.get("firehose", 0)
@@ -0,0 +1,214 @@
"""
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 time
from collections.abc import Callable
from cereal import custom
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import NavWidget, Widget
from openpilot.system.ui.widgets.scroller import Scroller
class ModelsLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
super().__init__()
self.set_back_callback(back_callback)
self.original_back_callback = back_callback
self.refresh_start_time = 0
self.focused_widget = None
self.current_model_btn = BigButton(tr("current model"), "", "")
self.current_model_btn.set_click_callback(self._show_folders)
self.refresh_btn = BigButton(tr("refresh model list"), "", "")
self.refresh_btn.set_click_callback(self._handle_refresh)
self.clear_cache_btn = BigButton(tr("clear model cache"), "", "")
self.clear_cache_btn.set_click_callback(self._handle_clear_cache)
self.cancel_download_btn = BigButton(tr("cancel download"), "", "")
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
self.lane_turn_toggle = BigToggle(text=tr("use lane turn desires"), initial_state=ui_state.params.get_bool("LaneTurnDesire"),
toggle_callback=lambda state: ui_state.params.put_bool("LaneTurnDesire", state))
self.lagd_toggle = BigToggle(text=tr("live learning steer delay"), initial_state=ui_state.params.get_bool("LagdToggle"),
toggle_callback=lambda state: ui_state.params.put_bool("LagdToggle", state))
self.lane_turn_value_btn = BigButton(tr("adjust lane turn speed"), "", "")
self.lane_turn_value_btn.set_click_callback(self._adjust_lane_turn)
self.delay_btn = BigButton(tr("adjust software delay"), "", "")
self.delay_btn.set_click_callback(self._adjust_delay)
self.main_items: list[Widget] = [self.current_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn, self.lane_turn_toggle,
self.lane_turn_value_btn, self.lagd_toggle, self.delay_btn]
self._scroller = Scroller(self.main_items, snap_items=False)
@property
def model_manager(self):
return ui_state.sm["modelManagerSP"]
def _get_grouped_bundles(self):
bundles = self.model_manager.availableBundles
folders = {}
for bundle in bundles:
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
folders.setdefault(folder, []).append(bundle)
return folders
def _show_selection_view(self, items: list[Widget], back_callback: Callable):
self._scroller._items = items
for item in items:
item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled)
self._scroller.scroll_panel.set_offset(0)
self.set_back_callback(back_callback)
def _show_folders(self):
self.focused_widget = self.current_model_btn
folders = self._get_grouped_bundles()
folder_buttons = []
default_btn = BigButton(tr("default model"), "", "")
default_btn.set_click_callback(self._select_default)
folder_buttons.append(default_btn)
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
if folder:
btn = BigButton(folder.lower(), "", "")
btn.set_click_callback(lambda f=folder: self._select_folder(f))
folder_buttons.append(btn)
self._show_selection_view(folder_buttons, self._reset_main_view)
def _handle_refresh(self):
self.refresh_btn.set_text(tr("refreshing..."))
self.refresh_start_time = time.monotonic()
ui_state.params.put("ModelManager_LastSyncTime", 0)
def _handle_clear_cache(self):
gui_app.set_modal_overlay(BigConfirmationDialogV2(tr("clear model cache?"), "icons_mici/settings/device/update.png",
confirm_callback=lambda: ui_state.params.put_bool("ModelManager_ClearCache", True)))
def _select_model(self, bundle):
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
self._reset_main_view()
def _select_default(self):
ui_state.params.remove("ModelManager_ActiveBundle")
self._reset_main_view()
def _select_folder(self, folder_name):
folders = self._get_grouped_bundles()
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
btns = []
for bundle in bundles:
txt = bundle.displayName.lower()
if self.model_manager.activeBundle and self.model_manager.activeBundle.index == bundle.index:
txt += " (active)"
elif bundle.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached):
txt += " (cached)"
btn = BigButton(txt, "", "")
btn.set_click_callback(lambda b=bundle: self._select_model(b))
btns.append(btn)
self._show_selection_view(btns, self._show_folders)
def _reset_main_view(self):
self._scroller._items = self.main_items
self.set_back_callback(self.original_back_callback)
if self.focused_widget and self.focused_widget in self.main_items:
x = self._scroller._pad_start
for item in self.main_items:
if not item.is_visible:
continue
if item == self.focused_widget:
break
x += item.rect.width + self._scroller._spacing
self._scroller.scroll_panel.set_offset(0)
self._scroller.scroll_to(x)
self.focused_widget = None
else:
self._scroller.scroll_panel.set_offset(0)
def _create_buttons(self, values, current_val, label, callback):
buttons = []
for value in values:
suffix = " (current)" if value == current_val else ""
btn = BigButton(f"{label(value)}{suffix}", "", "")
btn.set_click_callback(lambda v=value: callback(v))
buttons.append(btn)
return buttons
def _adjust_lane_turn(self):
self.focused_widget = self.lane_turn_value_btn
lane_turn_value = float(ui_state.params.get("LaneTurnValue", return_default=True))
is_metric = ui_state.is_metric
cur = int(round(lane_turn_value * CV.MPH_TO_KPH)) if is_metric else int(round(lane_turn_value))
values = [8, 16, 24, 32] if is_metric else [5, 10, 15, 20]
btns = self._create_buttons(values, cur, lambda v: f"{v} {'km/h' if is_metric else 'mph'}", self._set_lane_turn)
self._show_selection_view(btns, self._reset_main_view)
def _set_lane_turn(self, value):
val = value / CV.MPH_TO_KPH if ui_state.is_metric else float(value)
ui_state.params.put("LaneTurnValue", val)
self._reset_main_view()
def _adjust_delay(self):
self.focused_widget = self.delay_btn
current_delay = float(ui_state.params.get("LagdToggleDelay", return_default=True))
values = [round(i * 0.01, 2) for i in range(10, 31)]
btns = self._create_buttons(values, current_delay, lambda v: f"{v:.2f}s", self._set_delay)
self._show_selection_view(btns, self._reset_main_view)
def _set_delay(self, value):
ui_state.params.put("LagdToggleDelay", value)
self._reset_main_view()
def _update_state(self):
super()._update_state()
if self.refresh_start_time > 0 and time.monotonic() - self.refresh_start_time > 1:
self.refresh_btn.set_text(tr("refresh model list"))
self.refresh_start_time = 0
manager = self.model_manager
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
self.current_model_btn.set_value(f"downloading {manager.selectedBundle.displayName.lower()}")
self.cancel_download_btn.set_visible(True)
else:
self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model"))
self.cancel_download_btn.set_visible(False)
self.current_model_btn.set_enabled(ui_state.is_offroad())
self.current_model_btn.set_text(tr("current model"))
advanced_controls = ui_state.params.get_bool("ShowAdvancedControls")
turn_desires = ui_state.params.get_bool("LaneTurnDesire")
lagd_delay = ui_state.params.get_bool("LagdToggle")
self.lane_turn_value_btn.set_visible(turn_desires and advanced_controls)
if turn_desires and advanced_controls:
lane_turn_value = float(ui_state.params.get("LaneTurnValue", return_default=True))
val = int(round(lane_turn_value * CV.MPH_TO_KPH)) if ui_state.is_metric else int(round(lane_turn_value))
self.lane_turn_value_btn.set_text(tr("adjust lane turn speed"))
self.lane_turn_value_btn.set_value(f"{val} {'km/h' if ui_state.is_metric else 'mph'}")
self.delay_btn.set_visible(not lagd_delay and advanced_controls)
if not lagd_delay and advanced_controls:
toggle_delay = float(ui_state.params.get("LagdToggleDelay", return_default=True))
self.delay_btn.set_text(tr("adjust software delay"))
self.delay_btn.set_value(f"{toggle_delay:.2f}s")
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
super().show_event()
self._scroller.show_event()
@@ -36,8 +36,6 @@ class WifiIcon(Widget):
super().__init__() super().__init__()
self.set_rect(rl.Rectangle(0, 0, 89, 64)) self.set_rect(rl.Rectangle(0, 0, 89, 64))
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 89, 64)
self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 89, 64)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64) self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64) self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64)
@@ -57,17 +55,13 @@ class WifiIcon(Widget):
return return
# Determine which wifi strength icon to use # Determine which wifi strength icon to use
strength = round(self._network.strength / 100 * 4) strength = round(self._network.strength / 100 * 2)
if strength == 4: if strength == 2:
strength_icon = self._wifi_full_txt strength_icon = self._wifi_full_txt
elif strength == 3: elif strength == 1:
strength_icon = self._wifi_medium_txt strength_icon = self._wifi_medium_txt
elif strength == 2:
strength_icon = self._wifi_low_txt
elif self._network.strength < 0:
strength_icon = self._wifi_slash_txt
else: else:
strength_icon = self._wifi_none_txt strength_icon = self._wifi_low_txt
icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2) icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2)
icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2) icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2)
@@ -388,7 +382,7 @@ class WifiUIMici(BigMultiOptionDialog):
else: else:
network_button = WifiItem(network) network_button = WifiItem(network)
self.add_button(network_button) self._scroller.add_widget(network_button)
# remove networks no longer present # remove networks no longer present
self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks] self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks]
@@ -402,11 +396,10 @@ class WifiUIMici(BigMultiOptionDialog):
self._wifi_manager.connect_to_network(ssid, password) self._wifi_manager.connect_to_network(ssid, password)
self._update_buttons() self._update_buttons()
def _on_option_selected(self, option: str, smooth_scroll: bool = True): def _on_option_selected(self, option: str):
super()._on_option_selected(option, smooth_scroll) super()._on_option_selected(option)
# only open if button is already selected if option in self._networks:
if option in self._networks and option == self._selected_option:
self._network_info_page.set_current_network(self._networks[option]) self._network_info_page.set_current_network(self._networks[option])
self._open_network_manage_page() self._open_network_manage_page()
@@ -453,7 +446,7 @@ class WifiUIMici(BigMultiOptionDialog):
current_selection = self.get_selected_option() current_selection = self.get_selected_option()
if self._restore_selection and current_selection in self._networks: if self._restore_selection and current_selection in self._networks:
self._scroller._layout() self._scroller._layout()
BigMultiOptionDialog._on_option_selected(self, current_selection, smooth_scroll=False) BigMultiOptionDialog._on_option_selected(self, current_selection)
self._restore_selection = None self._restore_selection = None
super()._render(_) super()._render(_)
@@ -222,6 +222,9 @@ class AlertRenderer(Widget):
self._alert_y_filter.update(self._rect.y - 50 if alert is None else self._rect.y) self._alert_y_filter.update(self._rect.y - 50 if alert is None else self._rect.y)
self._alpha_filter.update(0 if alert is None else 1) self._alpha_filter.update(0 if alert is None else 1)
if gui_app.sunnypilot_ui():
ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
if alert is None: if alert is None:
# If still animating out, keep the previous alert # If still animating out, keep the previous alert
if self._alpha_filter.x > 0.01 and self._prev_alert is not None: if self._alpha_filter.x > 0.01 and self._prev_alert is not None:
@@ -19,6 +19,9 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera
from openpilot.common.transformations.orientation import rot_from_euler from openpilot.common.transformations.orientation import rot_from_euler
from enum import IntEnum from enum import IntEnum
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
OpState = log.SelfdriveState.OpenpilotState OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated CALIBRATED = log.LiveCalibrationData.Status.calibrated
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
@@ -351,6 +354,14 @@ class AugmentedRoadView(CameraView):
return self._cached_matrix return self._cached_matrix
def show_event(self):
if gui_app.sunnypilot_ui():
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME)
def hide_event(self):
if gui_app.sunnypilot_ui():
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE)
if __name__ == "__main__": if __name__ == "__main__":
gui_app.init_window("OnRoad Camera View") gui_app.init_window("OnRoad Camera View")
+8 -8
View File
@@ -71,7 +71,7 @@ class BigCircleButton(Widget):
class BigCircleToggle(BigCircleButton): class BigCircleToggle(BigCircleButton):
def __init__(self, icon: str, toggle_callback: Callable = None): def __init__(self, icon: str, toggle_callback: Callable | None = None):
super().__init__(icon, False) super().__init__(icon, False)
self._toggle_callback = toggle_callback self._toggle_callback = toggle_callback
@@ -251,7 +251,7 @@ class BigButton(Widget):
class BigToggle(BigButton): class BigToggle(BigButton):
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None): def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None):
super().__init__(text, value, "") super().__init__(text, value, "")
self._checked = initial_state self._checked = initial_state
self._toggle_callback = toggle_callback self._toggle_callback = toggle_callback
@@ -288,8 +288,8 @@ class BigToggle(BigButton):
class BigMultiToggle(BigToggle): class BigMultiToggle(BigToggle):
def __init__(self, text: str, options: list[str], toggle_callback: Callable = None, def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable = None): select_callback: Callable | None = None):
super().__init__(text, "", toggle_callback=toggle_callback) super().__init__(text, "", toggle_callback=toggle_callback)
assert len(options) > 0 assert len(options) > 0
self._options = options self._options = options
@@ -327,8 +327,8 @@ class BigMultiToggle(BigToggle):
class BigMultiParamToggle(BigMultiToggle): class BigMultiParamToggle(BigMultiToggle):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None, def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable = None): select_callback: Callable | None = None):
super().__init__(text, options, toggle_callback, select_callback) super().__init__(text, options, toggle_callback, select_callback)
self._param = param self._param = param
@@ -345,7 +345,7 @@ class BigMultiParamToggle(BigMultiToggle):
class BigParamControl(BigToggle): class BigParamControl(BigToggle):
def __init__(self, text: str, param: str, toggle_callback: Callable = None): def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
super().__init__(text, "", toggle_callback=toggle_callback) super().__init__(text, "", toggle_callback=toggle_callback)
self.param = param self.param = param
self.params = Params() self.params = Params()
@@ -361,7 +361,7 @@ class BigParamControl(BigToggle):
# TODO: param control base class # TODO: param control base class
class BigCircleParamControl(BigCircleToggle): class BigCircleParamControl(BigCircleToggle):
def __init__(self, icon: str, param: str, toggle_callback: Callable = None): def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None):
super().__init__(icon, toggle_callback) super().__init__(icon, toggle_callback)
self._param = param self._param = param
self.params = Params() self.params = Params()
+38 -17
View File
@@ -4,17 +4,17 @@ import pyray as rl
from typing import Union from typing import Union
from collections.abc import Callable from collections.abc import Callable
from typing import cast from typing import cast
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent
from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
DEBUG = False DEBUG = False
@@ -137,7 +137,7 @@ class BigInputDialog(BigDialogBase):
hint: str, hint: str,
default_text: str = "", default_text: str = "",
minimum_length: int = 1, minimum_length: int = 1,
confirm_callback: Callable[[str], None] = None): confirm_callback: Callable[[str], None] | None = None):
super().__init__(None, None) super().__init__(None, None)
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)), self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
font_weight=FontWeight.MEDIUM) font_weight=FontWeight.MEDIUM)
@@ -317,39 +317,36 @@ class BigMultiOptionDialog(BigDialogBase):
BACK_TOUCH_AREA_PERCENTAGE = 0.1 BACK_TOUCH_AREA_PERCENTAGE = 0.1
def __init__(self, options: list[str], default: str | None, def __init__(self, options: list[str], default: str | None,
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] = None): right_btn: str | None = 'check', right_btn_callback: Callable[[], None] | None = None):
super().__init__(right_btn, right_btn_callback=right_btn_callback) super().__init__(right_btn, right_btn_callback=right_btn_callback)
self._options = options self._options = options
if default is not None: if default is not None:
assert default in options assert default in options
self._default_option: str = default or (options[0] if len(options) > 0 else "") self._default_option: str | None = default
self._selected_option: str = self._default_option self._selected_option: str = self._default_option or (options[0] if len(options) > 0 else "")
self._last_selected_option: str = self._selected_option self._last_selected_option: str = self._selected_option
# Widget doesn't differentiate between click and drag
self._can_click = True
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True) self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
if self._right_btn is not None: if self._right_btn is not None:
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed) self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
for option in options: for option in options:
self.add_button(BigDialogOptionButton(option)) self._scroller.add_widget(BigDialogOptionButton(option))
def add_button(self, button: BigDialogOptionButton):
def click_callback(_btn=button):
self._on_option_selected(_btn.option)
button.set_click_callback(click_callback)
self._scroller.add_widget(button)
def show_event(self): def show_event(self):
super().show_event() super().show_event()
self._scroller.show_event() self._scroller.show_event()
self._on_option_selected(self._default_option) if self._default_option is not None:
self._on_option_selected(self._default_option)
def get_selected_option(self) -> str: def get_selected_option(self) -> str:
return self._selected_option return self._selected_option
def _on_option_selected(self, option: str, smooth_scroll: bool = True): def _on_option_selected(self, option: str):
y_pos = 0.0 y_pos = 0.0
for btn in self._scroller._items: for btn in self._scroller._items:
btn = cast(BigDialogOptionButton, btn) btn = cast(BigDialogOptionButton, btn)
@@ -365,11 +362,35 @@ class BigMultiOptionDialog(BigDialogBase):
y_pos = rect_center_y - (btn.rect.y + height / 2) y_pos = rect_center_y - (btn.rect.y + height / 2)
break break
self._scroller.scroll_to(-y_pos, smooth=smooth_scroll) self._scroller.scroll_to(-y_pos)
def _selected_option_changed(self): def _selected_option_changed(self):
pass pass
def _handle_mouse_press(self, mouse_pos: MousePos):
super()._handle_mouse_press(mouse_pos)
self._can_click = True
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
super()._handle_mouse_event(mouse_event)
# # TODO: add generic _handle_mouse_click handler to Widget
if not self._scroller.scroll_panel.is_touch_valid():
self._can_click = False
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
if not self._can_click:
return
# select current option
for btn in self._scroller._items:
btn = cast(BigDialogOptionButton, btn)
if btn.option == self._selected_option:
self._on_option_selected(btn.option)
break
def _update_state(self): def _update_state(self):
super()._update_state() super()._update_state()
+4
View File
@@ -116,6 +116,10 @@ class AlertRenderer(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
alert = self.get_alert(ui_state.sm) alert = self.get_alert(ui_state.sm)
if gui_app.sunnypilot_ui():
ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
if not alert: if not alert:
return return
+11 -3
View File
@@ -15,10 +15,10 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera
from openpilot.common.transformations.orientation import rot_from_euler from openpilot.common.transformations.orientation import rot_from_euler
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP
from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
OpState = log.SelfdriveState.OpenpilotState OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated CALIBRATED = log.LiveCalibrationData.Status.calibrated
@@ -224,6 +224,14 @@ class AugmentedRoadView(CameraView):
return self._cached_matrix return self._cached_matrix
def show_event(self):
if gui_app.sunnypilot_ui():
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME)
def hide_event(self):
if gui_app.sunnypilot_ui():
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE)
if __name__ == "__main__": if __name__ == "__main__":
gui_app.init_window("OnRoad Camera View") gui_app.init_window("OnRoad Camera View")
@@ -4,9 +4,21 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
from enum import IntEnum
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, ToggleActionSP
ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 15, 1: 30, **{i: (i - 1) * 60 for i in range(2, 12)}}
class OnroadBrightness(IntEnum):
AUTO = 0
AUTO_DARK = 1
class DisplayLayout(Widget): class DisplayLayout(Widget):
@@ -18,11 +30,69 @@ class DisplayLayout(Widget):
self._scroller = Scroller(items, line_separator=True, spacing=0) self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self): def _initialize_items(self):
self._onroad_brightness = option_item_sp(
param="OnroadScreenOffBrightness",
title=lambda: tr("Onroad Brightness"),
description="",
min_value=0,
max_value=21,
value_change_step=1,
label_callback=lambda value: self.update_onroad_brightness(value),
inline=True
)
self._onroad_brightness_timer = option_item_sp(
param="OnroadScreenOffTimer",
title=lambda: tr("Onroad Brightness Delay"),
description="",
min_value=0,
max_value=11,
value_change_step=1,
value_map=ONROAD_BRIGHTNESS_TIMER_VALUES,
label_callback=lambda value: f"{value} s" if value < 60 else f"{int(value/60)} m",
inline=True
)
self._interactivity_timeout = option_item_sp(
param="InteractivityTimeout",
title=lambda: tr("Interactivity Timeout"),
description=lambda: tr("Apply a custom timeout for settings UI." +
"<br>This is the time after which settings UI closes automatically " +
"if user is not interacting with the screen."),
min_value=0,
max_value=120,
value_change_step=10,
label_callback=lambda value: (tr("Default") if not value or value == 0 else
f"{value} s" if value < 60 else f"{int(value/60)} m"),
inline=True
)
items = [ items = [
self._onroad_brightness,
self._onroad_brightness_timer,
self._interactivity_timeout,
] ]
return items return items
@staticmethod
def update_onroad_brightness(val):
if val == OnroadBrightness.AUTO:
return tr("Auto (Default)")
if val == OnroadBrightness.AUTO_DARK:
return tr("Auto (Dark)")
return f"{(val - 1) * 5} %"
def _update_state(self):
super()._update_state()
for _item in self._scroller._items:
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None:
_item.action_item.set_state(self._params.get_bool(_item.action_item.toggle.param_key))
elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
_item.action_item.set_value(self._params.get(_item.action_item.param_key, return_default=True))
brightness_val = self._params.get("OnroadScreenOffBrightness", return_default=True)
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
def _render(self, rect): def _render(self, rect):
self._scroller.render(rect) self._scroller.render(rect)
@@ -226,9 +226,7 @@ class ModelsLayout(Widget):
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire") turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
live_delay: bool = ui_state.params.get_bool("LagdToggle") live_delay: bool = ui_state.params.get_bool("LagdToggle")
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls) self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
self.lagd_toggle.action_item.set_state(live_delay)
self.delay_control.set_visible(not live_delay and advanced_controls) self.delay_control.set_visible(not live_delay and advanced_controls)
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100 new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
if self.lane_turn_value_control.action_item.value_change_step != new_step: if self.lane_turn_value_control.action_item.value_change_step != new_step:
@@ -37,7 +37,7 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255) OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
ICON_SIZE = 70 ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore OP.PanelType = IntEnum(
"PanelType", "PanelType",
[es.name for es in OP.PanelType] + [ [es.name for es in OP.PanelType] + [
"SUNNYLINK", "SUNNYLINK",
@@ -336,7 +336,6 @@ class SunnylinkLayout(Widget):
self._sunnylink_enabled = ui_state.params.get_bool("SunnylinkEnabled") self._sunnylink_enabled = ui_state.params.get_bool("SunnylinkEnabled")
self._sunnylink_toggle.set_right_value(tr("Dongle ID") + ": " + self._get_sunnylink_dongle_id()) self._sunnylink_toggle.set_right_value(tr("Dongle ID") + ": " + self._get_sunnylink_dongle_id())
self._sunnylink_toggle.action_item.set_enabled(not ui_state.is_onroad()) self._sunnylink_toggle.action_item.set_enabled(not ui_state.is_onroad())
self._sunnylink_toggle.action_item.set_state(self._sunnylink_enabled)
self._sunnylink_uploader_toggle.action_item.set_enabled(self._sunnylink_enabled) self._sunnylink_uploader_toggle.action_item.set_enabled(self._sunnylink_enabled)
self.handle_backup_restore_progress() self.handle_backup_restore_progress()
@@ -55,5 +55,4 @@ class HyundaiSettings(BrandSettings):
self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled) self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled)
self.longitudinal_tuning_item.set_description(long_tuning_desc) self.longitudinal_tuning_item.set_description(long_tuning_desc)
self.longitudinal_tuning_item.show_description(True) self.longitudinal_tuning_item.show_description(True)
self.longitudinal_tuning_item.action_item.set_selected_button(tuning_param)
self.longitudinal_tuning_item.set_visible(self.alpha_long_available) self.longitudinal_tuning_item.set_visible(self.alpha_long_available)
@@ -9,13 +9,15 @@ from enum import IntEnum
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.models import ModelsLayoutMici
ICON_SIZE = 70 ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore OP.PanelType = IntEnum(
"PanelType", "PanelType",
[es.name for es in OP.PanelType] + [ [es.name for es in OP.PanelType] + [
"SUNNYLINK", "SUNNYLINK",
"MODELS",
], ],
start=0, start=0,
) )
@@ -27,13 +29,17 @@ class SettingsLayoutSP(OP.SettingsLayout):
sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png") sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png")
sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK)) sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK))
models_btn = BigButton("models", "", "../../sunnypilot/selfdrive/assets/offroad/icon_models.png")
models_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.MODELS))
self._panels.update({ self._panels.update({
OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))), OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
OP.PanelType.MODELS: OP.PanelInfo("models", ModelsLayoutMici(back_callback=lambda: self._set_current_panel(None))),
}) })
items = self._scroller._items.copy() items = self._scroller._items.copy()
items.insert(1, sunnylink_btn) items.insert(1, sunnylink_btn)
items.insert(2, models_btn)
self._scroller._items.clear() self._scroller._items.clear()
for item in items: for item in items:
self._scroller.add_widget(item) self._scroller.add_widget(item)
@@ -18,11 +18,12 @@ class RainbowPath:
BASE_ALPHA = 0.8 BASE_ALPHA = 0.8
ALPHA_FADE = 0.3 # Alpha reduction from bottom to top ALPHA_FADE = 0.3 # Alpha reduction from bottom to top
def __init__(self, num_segments: int = None, speed: float = None, saturation: float = None, lightness: float = None): def __init__(self, num_segments: int = DEFAULT_NUM_SEGMENTS, speed: float = DEFAULT_SPEED,
self.num_segments = num_segments if num_segments is not None else self.DEFAULT_NUM_SEGMENTS saturation: float = DEFAULT_SATURATION, lightness: float = DEFAULT_LIGHTNESS):
self.speed = speed if speed is not None else self.DEFAULT_SPEED self.num_segments = num_segments
self.saturation = saturation if saturation is not None else self.DEFAULT_SATURATION self.speed = speed
self.lightness = lightness if lightness is not None else self.DEFAULT_LIGHTNESS self.saturation = saturation
self.lightness = lightness
def set_speed(self, speed: float): def set_speed(self, speed: float):
self.speed = speed self.speed = speed
+41
View File
@@ -0,0 +1,41 @@
"""
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.
"""
def update_item_from_param(item, key, params):
if not (action := getattr(item, 'action_item', None)):
return
if hasattr(action, 'set_state'):
action.set_state(params.get_bool(key))
elif hasattr(action, 'set_value'):
action.set_value(params.get(key, return_default=True))
else:
try:
val = int(params.get(key, return_default=True))
if hasattr(action, 'selected_button'):
action.selected_button = val
if hasattr(action, 'current_value'):
action.current_value = val
except (ValueError, TypeError):
pass
def sync_layout_params(layout, param_name, params):
targets = []
if toggles := getattr(layout, '_toggles', None):
targets.extend([(item, k) for k, item in toggles.items()])
items = getattr(layout, 'items', []) or getattr(getattr(layout, '_scroller', None), '_items', [])
for item in items:
action = getattr(item, 'action_item', None)
if key := getattr(action, 'param_key', None) or getattr(getattr(action, 'toggle', None), 'param_key', None):
targets.append((item, key))
for item, key in targets:
if param_name is None or key == param_name:
update_item_from_param(item, key, params)
+116 -3
View File
@@ -4,26 +4,55 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
from enum import Enum
from cereal import messaging, log, custom from cereal import messaging, log, custom
from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.common.params import Params
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.sunnypilot.ui_helpers import sync_layout_params
OpenpilotState = log.SelfdriveState.OpenpilotState OpenpilotState = log.SelfdriveState.OpenpilotState
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
ONROAD_BRIGHTNESS_TIMER_PAUSED = -1
class OnroadTimerStatus(Enum):
NONE = 0
PAUSE = 1
RESUME = 2
class UIStateSP: class UIStateSP:
def __init__(self): def __init__(self):
self.params = Params() self.params = Params()
self.params.add_watcher(self.on_param_change)
self.params.start()
self.sm_services_ext = [ self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay" "gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
] ]
self.sunnylink_state = SunnylinkState() self.sunnylink_state = SunnylinkState()
self.update_params()
self.active_layout = None
self.changed_params = set()
def set_active_layout(self, layout):
self.active_layout = layout
if layout:
sync_layout_params(layout, None, self.params)
def on_param_change(self, param_name):
self.changed_params.add(param_name)
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True) self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.global_brightness_override: int = self.params.get("Brightness", return_default=True) self.reset_onroad_sleep_timer()
def update(self) -> None: def update(self) -> None:
if self.sunnylink_enabled: if self.sunnylink_enabled:
@@ -31,6 +60,52 @@ class UIStateSP:
else: else:
self.sunnylink_state.stop() self.sunnylink_state.stop()
def onroad_brightness_handle_alerts(self, started: bool, alert):
has_alert = started and self.onroad_brightness != OnroadBrightness.AUTO and alert is not None
self.update_onroad_brightness(has_alert)
if has_alert:
self.reset_onroad_sleep_timer()
def update_onroad_brightness(self, has_alert: bool) -> None:
if has_alert:
return
if self.onroad_brightness_timer > 0:
self.onroad_brightness_timer -= 1
def reset_onroad_sleep_timer(self, timer_status: OnroadTimerStatus = OnroadTimerStatus.NONE) -> None:
# Toggling from active state to inactive
if timer_status == OnroadTimerStatus.PAUSE and self.onroad_brightness_timer != ONROAD_BRIGHTNESS_TIMER_PAUSED:
self.onroad_brightness_timer = ONROAD_BRIGHTNESS_TIMER_PAUSED
# Toggling from a previously inactive state or resetting an active timer
elif (self.onroad_brightness_timer_param >= 0 and self.onroad_brightness != OnroadBrightness.AUTO and
self.onroad_brightness_timer != ONROAD_BRIGHTNESS_TIMER_PAUSED) or timer_status == OnroadTimerStatus.RESUME:
if self.onroad_brightness == OnroadBrightness.AUTO_DARK:
self.onroad_brightness_timer = 15 * gui_app.target_fps
else:
self.onroad_brightness_timer = self.onroad_brightness_timer_param * gui_app.target_fps
@property
def onroad_brightness_timer_expired(self) -> bool:
return self.onroad_brightness != OnroadBrightness.AUTO and self.onroad_brightness_timer == 0
@property
def auto_onroad_brightness(self) -> bool:
return self.onroad_brightness in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)
if not self.params.is_watching():
cloudlog.warning("ParamWatcher thread died, restarting...")
self.params.start()
if self.changed_params:
while self.changed_params:
self.changed_params.pop()
if self.active_layout:
sync_layout_params(self.active_layout, None, self.params)
@staticmethod @staticmethod
def update_status(ss, ss_sp, onroad_evt) -> str: def update_status(ss, ss_sp, onroad_evt) -> str:
state = ss.state state = ss.state
@@ -78,7 +153,10 @@ class UIStateSP:
self.chevron_metrics = self.params.get("ChevronInfo") self.chevron_metrics = self.params.get("ChevronInfo")
self.active_bundle = self.params.get("ModelManager_ActiveBundle") self.active_bundle = self.params.get("ModelManager_ActiveBundle")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
self.global_brightness_override = self.params.get("Brightness", return_default=True)
# Onroad Screen Brightness
self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True)))
self.onroad_brightness_timer_param = self.params.get("OnroadScreenOffTimer", return_default=True)
class DeviceSP: class DeviceSP:
@@ -88,3 +166,38 @@ class DeviceSP:
def _set_awake(self, on: bool): def _set_awake(self, on: bool):
if on and self._params.get("DeviceBootMode", return_default=True) == 1: if on and self._params.get("DeviceBootMode", return_default=True) == 1:
self._params.put_bool("OffroadMode", True) self._params.put_bool("OffroadMode", True)
@staticmethod
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
if not awake or not _ui_state.started:
return cur_brightness
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)
if _ui_state.onroad_brightness == OnroadBrightness.AUTO:
return cur_brightness
elif _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
return cur_brightness
# 2-21: 5% - 100%
return float((_ui_state.onroad_brightness - 1) * 5)
@staticmethod
def set_min_onroad_brightness(_ui_state, min_brightness: int) -> int:
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
min_brightness = 10
return min_brightness
@staticmethod
def wake_from_dimmed_onroad_brightness(_ui_state, evs) -> None:
if _ui_state.started and (_ui_state.onroad_brightness_timer_expired or _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK):
if any(ev.left_down for ev in evs):
if _ui_state.onroad_brightness_timer_expired:
gui_app.mouse_events.clear()
_ui_state.reset_onroad_sleep_timer()
+3
View File
@@ -0,0 +1,3 @@
# Multilanguage
[![languages](https://raw.githubusercontent.com/commaai/openpilot/badges/translation_badge.svg)](#)
+3 -3
View File
@@ -200,7 +200,7 @@ msgstr "CONNECT"
#: system/ui/widgets/network.py:369 #: system/ui/widgets/network.py:369
#, python-format #, python-format
msgid "CONNECTING..." msgid "CONNECTING..."
msgstr "CONNECTING..." msgstr "连接中..."
#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 #: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35
#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 #: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318
@@ -1001,7 +1001,7 @@ msgstr "用于“{}”"
#: selfdrive/ui/onroad/hud_renderer.py:177 #: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format #, python-format
msgid "km/h" msgid "km/h"
msgstr "公里/时" msgstr "km/h"
#: system/ui/widgets/network.py:204 #: system/ui/widgets/network.py:204
#, python-format #, python-format
@@ -1021,7 +1021,7 @@ msgstr "计量"
#: selfdrive/ui/onroad/hud_renderer.py:177 #: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format #, python-format
msgid "mph" msgid "mph"
msgstr "英里/时" msgstr "mph"
#: selfdrive/ui/layouts/settings/software.py:20 #: selfdrive/ui/layouts/settings/software.py:20
#, python-format #, python-format
+3 -3
View File
@@ -200,7 +200,7 @@ msgstr "CONNECT"
#: system/ui/widgets/network.py:369 #: system/ui/widgets/network.py:369
#, python-format #, python-format
msgid "CONNECTING..." msgid "CONNECTING..."
msgstr "CONNECTING..." msgstr "連線中..."
#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 #: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35
#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 #: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318
@@ -1000,7 +1000,7 @@ msgstr "適用於「{}」"
#: selfdrive/ui/onroad/hud_renderer.py:177 #: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format #, python-format
msgid "km/h" msgid "km/h"
msgstr "公里/時" msgstr "km/h"
#: system/ui/widgets/network.py:204 #: system/ui/widgets/network.py:204
#, python-format #, python-format
@@ -1020,7 +1020,7 @@ msgstr "計量"
#: selfdrive/ui/onroad/hud_renderer.py:177 #: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format #, python-format
msgid "mph" msgid "mph"
msgstr "英里/時" msgstr "mph"
#: selfdrive/ui/layouts/settings/software.py:20 #: selfdrive/ui/layouts/settings/software.py:20
#, python-format #, python-format
+1 -1
View File
@@ -18,7 +18,7 @@ OPENAI_PROMPT = "You are a professional translator from English to {language} (I
"The following sentence or word is in the GUI of a software called openpilot, translate it accordingly." "The following sentence or word is in the GUI of a software called openpilot, translate it accordingly."
def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]: def get_language_files(languages: list[str] | None = None) -> dict[str, pathlib.Path]:
files = {} files = {}
with open(TRANSLATIONS_LANGUAGES) as fp: with open(TRANSLATIONS_LANGUAGES) as fp:
+9 -9
View File
@@ -6,7 +6,6 @@ from collections.abc import Callable
from enum import Enum from enum import Enum
from cereal import messaging, car, log from cereal import messaging, car, log
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
@@ -36,7 +35,6 @@ class UIState(UIStateSP):
def _initialize(self): def _initialize(self):
UIStateSP.__init__(self) UIStateSP.__init__(self)
self.params = Params()
self.sm = messaging.SubMaster( self.sm = messaging.SubMaster(
[ [
"modelV2", "modelV2",
@@ -258,17 +256,16 @@ class Device(DeviceSP):
else: else:
clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0 clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0
min_brightness = 30
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
if ui_state.global_brightness_override <= 0: min_brightness = DeviceSP.set_min_onroad_brightness(ui_state, min_brightness)
min_global_brightness = 1 if ui_state.global_brightness_override < 0 else 30
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [min_global_brightness, 100])) clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [min_brightness, 100]))
else:
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100]))
brightness = round(self._brightness_filter.update(clipped_brightness)) brightness = round(self._brightness_filter.update(clipped_brightness))
if gui_app.sunnypilot_ui() and ui_state.global_brightness_override > 0: if gui_app.sunnypilot_ui():
brightness = ui_state.global_brightness_override brightness = DeviceSP.set_onroad_brightness(ui_state, self._awake, brightness)
if not self._awake: if not self._awake:
brightness = 0 brightness = 0
@@ -285,6 +282,9 @@ class Device(DeviceSP):
self._ignition = ui_state.ignition self._ignition = ui_state.ignition
if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events): if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events):
if gui_app.sunnypilot_ui():
DeviceSP.wake_from_dimmed_onroad_brightness(ui_state, gui_app.mouse_events)
self._reset_interactive_timeout() self._reset_interactive_timeout()
interaction_timeout = time.monotonic() > self._interaction_time interaction_timeout = time.monotonic() > self._interaction_time
+1
View File
@@ -1,3 +1,4 @@
SConscript(['common/transformations/SConscript'])
SConscript(['modeld/SConscript']) SConscript(['modeld/SConscript'])
SConscript(['modeld_v2/SConscript']) SConscript(['modeld_v2/SConscript'])
SConscript(['selfdrive/locationd/SConscript']) SConscript(['selfdrive/locationd/SConscript'])
+146
View File
@@ -0,0 +1,146 @@
<center>
# Comparative Analysis of Parameter Getter Methods: Params vs. ParamWatcher
James (sunnypilot Developer) <br>
December 13, 2025
</center>
## <br><br> Abstract
This research report examines the inefficiencies in standard parameter access methods within sunnypilot and proposes an optimized alternative, ParamWatcher. The standard `Params::get()` method incurs significant CPU and memory overhead due to repeated file I/O operations (sunnypilot, 2025). ParamWatcher utilizes OS-level file system events (inotify on Linux, FSEvents on macOS) to maintain an in-memory cache, reducing I/O to near zero (Linux man-pages, 2025-c; Apple Inc., n.d.-a). Empirical benchmarks with ~10 million parameter accesses demonstrate a 14.5x CPU speedup and flat memory usage (514.7 KB vs. 497.7 KB for base Params, with only 17 KB overhead). The implementation employs a process-local singleton pattern for efficiency in multi-process architectures (Gamma et al., 1994). Results indicate ParamWatcher eliminates UI stutters and GC pauses, enhancing system responsiveness without compromising data freshness.
**Keywords:** parameter access, file I/O optimization, event-driven caching, autonomous driving systems, performance benchmarking
## Introduction
In sunnypilot, efficient parameter management is important for real-time system access. The standard `Params::get()` method, implemented in C++ and wrapped in Python, performs full file I/O cycles for each access, leading to high CPU overhead and memory churn (sunnypilot, 2025). This is particularly problematic in UI loops where parameters are queried frequently (e.g., 50 toggles at 20 FPS equates to ~1,000 reads/second).
This inefficiency stems from architectural mismatches: C++ streams are designed for throughput, not latency (cppreference.com, n.d.-a). Each call triggers kernel mode switches, heap allocations, and garbage collection in Python, causing UI stutters (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
### Inefficiencies in Standard Parameter Access
The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop.
#### System Overhead Analysis
- **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025).
- **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
- **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025).
- **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.-b).
This report introduces ParamWatcher, an event-driven caching solution using OS file system events. It shifts from polling to notifications, caching converted values in static RAM. I propose that ParamWatcher achieves minimum 10x+ CPU gains with bounded non increasing memory, improving sunnypilot's performance both on latency and responsiveness.
## Method
### Materials
- **System:** sunnypilot, running on macOS, Ubuntu/Linux, comma 3x, and comma four.
- **Parameters:** 231 defined keys in `param_keys.h`.
- **Tools:** Python 3, tracemalloc for memory profiling, time.perf_counter for CPU timing, ctypes for OS integration (Python Software Foundation, 2025).
### Implementation Details
ParamWatcher provides cross-platform file system monitoring using ctypes for direct OS integration (Python Software Foundation, 2025).
#### Linux Implementation
On Linux, ParamWatcher uses the inotify subsystem for efficient file change detection (Linux man-pages, 2025-c). It loads `libc.so.6` to access system calls, initializes an inotify instance, and watches the parameters directory for events like `IN_MODIFY` and `IN_CLOSE_WRITE` (Linux Kernel Organization, 2005). Events are polled with `select.epoll()` and parsed using `struct.unpack_from()` to avoid ctypes overhead. Filenames are extracted and passed to cache invalidation, ensuring real-time updates without polling (Codezup, 2025).
- **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls.
- **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor.
- **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes.
- **Event Loop**:
- **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting.
- **Reading**: When events occur, `os.read(fd, 1024)` retrieves the raw binary event data.
- **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures.
- **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access.
#### macOS Implementation
On macOS, ParamWatcher leverages FSEvents from CoreServices for directory monitoring (Apple Inc., n.d.-a). It defines a C-compatible callback using `CFUNCTYPE`, creates an `FSEventStream` with `kFSEventStreamCreateFlagFileEvents`, and schedules it on the run loop (Apple Inc., n.d.-b). Events are filtered for modifications, creations, and renames (Apple Inc., n.d.-c), triggering cache invalidation for affected parameters.
- **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`.
- **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory.
- **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available.
- **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c).
- **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b).
- **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary.
- **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter.
### Procedure
Benchmarks simulated heavy load:
- **Memory Test:** ~10 million gets (43,290 loops over 231 keys), measured with tracemalloc.
- **CPU Test:** Same load, timed with perf_counter.
- Comparisons: Base Params vs. ParamWatcher.
## Results
### Memory Usage
Using tracemalloc for peak memory measurement during ~10 million parameter accesses (43,290 loops over 231 keys), base Params peaked at 497.7 KB, while ParamWatcher peaked at 514.7 KB (17 KB overhead). ParamWatcher's memory remained flat post-initialization, preventing churn.
| Condition | Memory (KB) | Overhead |
|-----------|-------------|----------|
| Base Params | 497.7 | - |
| ParamWatcher | 514.7 | 17 KB |
### CPU Performance
ParamWatcher was 14.5x faster: 4.52s vs. 65.43s to complete ~10 million param gets.
| Condition | Time (s) | Speedup |
|-----------|----------|---------|
| Base Params | 65.43 | 1x |
| ParamWatcher | 4.52 | 14.5x |
### Scalability
No degradation at scale; cache invalidation maintained freshness.
See Appendix A for visual graphs of memory usage over a 30-minute time span, captured on comma four. These two routes are of equal conditions: each route started completely unplugged to two minutes offroad, followed by onroad state, with ambient temperature at 75 degrees fahrenheit. These routes are direct comparisons of pre-ParamWatcher and ParamWatcher implementations. Appendix A also includes I/O capture graphs for those 30-minute routes, demonstrating reductions in file system activity post-ParamWatcher.
## Discussion
ParamWatcher successfully optimizes parameter access, delivering substantial CPU gains with minimal memory overhead. The event-driven approach eliminates I/O bottlenecks, reducing GC pressure and UI stutters (cppreference.com, n.d.-b). The 17 KB memory overhead is negligible compared to the megabytes of churn from base Params, ensuring bounded usage in multi-process environments via the singleton pattern (Gamma et al., 1994).
Results demonstrate scalability without degradation, with cache invalidation maintaining data freshness. This optimization enhances system responsiveness.
Limitations include potential event latency in high-load scenarios (<10 ms, imperceptible for UI) and increased complexity from background threads.
Trade-offs: Static RAM (~17 KB) vs. dynamic churn; benefits outweigh costs for param-heavy workloads.
## <br> Appendix A: Memory Usage Graphs
### Base Params Memory Usage
![Base Params Memory Usage](assets/memory_usage_pre_paramwatcher.png)
### ParamWatcher Memory Usage
![ParamWatcher Memory Usage](assets/memory_usage_00000025--d50a4a3471.png)
### Base Params IO Usage
![Base Params IO Usage](assets/io_usage_pre_paramwatcher.png)
### ParamWatcher IO Usage
![Base Params IO Usage](assets/io_usage_param_watcher.png)
## <br>References
Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events
Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop
Apple Inc. (n.d.-c). *FSEventStreamEventFlags*. Retrieved from https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags
Codezup. (2025). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/
cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cppreference.com/w/cpp/io/basic_ifstream
cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
Linux Kernel Organization. (2005). *include/uapi/linux/inotify.h*. Retrieved from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/inotify.h
Linux man-pages. (2025-a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html
Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html
Linux man-pages. (2025-c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html
Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html
sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2
sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc#L79C1-L117C2
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b94f93515223b41fba5250ffb7c795ebb1640c524de5296393b02bd36a202a18
size 566837
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:696dc900066c78de38e64f0636716a588f68f2fa4a3d4fbfdb5ca3f8ab49e922
size 291424
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abb4a5ec3108d337cb4aab0d009c5dd7c03f601f725188fcb933f7dc4cd1b1fa
size 248658
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a27f8ba0633e806083ce41bb51fbabcd027f8ad713534dc04298cc2f350b3389
size 311370

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