Compare commits

..

112 Commits

Author SHA1 Message Date
rav4kumar acd32dd032 rr 2026-01-25 11:54:39 -07:00
Kumar ec9fd534df Refine acceleration profiles and smoothing parameters
Adjusted acceleration profiles and breakpoints for different personalities. Modified smoothing alpha for acceleration.
2026-01-24 07:44:03 -07:00
rav4kumar 5eb7f9b901 ref 2026-01-23 07:15:01 -07:00
rav4kumar 04c03bc048 Revert "Revert "Revert "latcontrol_torque: lower kp and lower friction threshold (commaai/openpilot#36619)" (#1581)""
This reverts commit 0ca7e9540455e4be50c52f0e52205b694f32b705.
2026-01-23 07:11:58 -07:00
rav4kumar 662987038e Revert "Modeld: less lat smoothing"
This reverts commit f063379d2e66730e4930a57ddf7f0d9b28ac5f30.
2026-01-23 07:11:58 -07:00
Kumar ee8c68ee2b Fix MIN_ACCEL_BREAKPOINTS and update braking profiles 2026-01-23 07:11:58 -07:00
rav4kumar 1b47b75fab Revert "Revert "latcontrol_torque: lower kp and lower friction threshold (commaai/openpilot#36619)" (#1581)"
This reverts commit 7560497f
2026-01-23 07:11:55 -07:00
Kumar 1f75027084 Adjust acceleration and braking profiles for sport mode 2026-01-23 07:11:51 -07:00
Kumar 6395c07f62 Update acceleration and braking profiles 2026-01-23 07:11:51 -07:00
rav4kumar 708bb95e5d tune 2026-01-23 07:11:51 -07:00
rav4kumar e422bc6a10 show some love to c3x
dont swipe

yurr

Revert "mapd llk"

This reverts commit 7d27e37a3d7e713d00cc97c0388b1b4d571a597a.

new mapd ui for c4

mapd llk

Update services.py

Change mapdOut queue size to MEDIUM

Add files via upload

Delete selfdrive/mapd

207 lmfao

206 unrelease

203

Rename mapd 2 to mapd

2.0.4 msgq comparability

mapd ui
2026-01-23 07:11:51 -07:00
rav4kumar b448e3834c add mapd 2.0 2026-01-23 07:11:49 -07:00
rav4kumar 5165ba4851 remove old mapd, sla, scc 2026-01-23 07:11:45 -07:00
rav4kumar 8c1fd63a24 Refactor: Address PR #1573 review feedback
- Add ACCEL_PERSONALITY_OPTIONS global variable
- Move accel_controller and dynamic_follow logic out of stock
- Add get_accel_clip(), get_cruise_min_accel(), get_t_follow() methods to LongitudinalPlannerSP
- Stock long_mpc.py now accepts optional a_cruise_min and t_follow
2026-01-23 07:10:54 -07:00
rav4kumar 6217dd31d1 tune 2026-01-23 07:10:52 -07:00
rav4kumar 4a88664ea0 sla clean up and fade in/out 2026-01-23 07:10:50 -07:00
rav4kumar 0f4d7a7875 sla ui 2026-01-23 07:10:50 -07:00
rav4kumar 1609050b79 always bsm 2026-01-23 07:10:49 -07:00
rav4kumar 065b63ffd7 toyota sp link 2026-01-23 07:10:49 -07:00
rav4kumar 50f3e3173d misc. tune
no lead dempaning

ref

Update acceleration and braking profiles

slight adjustments to decel

exponential and mv avg

tune

improve accel limits and lead dampening

bpvs

Refactor follow distance profiles and smoothing logic

Updated follow distance profiles and added smoothing factor for dynamic follow behavior.

Refactor acceleration profiles and smoothing logic

Updated acceleration and braking profiles for different personalities, added smoothing and rate limiting for acceleration adjustments.

reft

tun

adjust accel and braking profiles

Update accel_controller.py

slight adjustment

try lower and no expno

Revise follow distance profiles and smoothing parameters

Updated follow distance profiles and breakpoints for different personalities. Adjusted smoothing parameters for better performance.

Refactor acceleration and braking profiles

lower ki and no smoothing to last min call

try

Update dynamic_follow.py

new new

fff

Update accel_controller.py

try

what if

just try

df exponential smoothing

exponential smoothing

tt

Update custom.capnp

conflic

ff

ref

rebase fix
2026-01-23 07:10:49 -07:00
rav4kumar 71b44dfda8 point the submodule 2026-01-23 07:10:49 -07:00
rav4kumar a0c46a899b feat/relc 2026-01-23 07:10:49 -07:00
rav4kumar b2ea3a4236 drive mode btn support 2026-01-23 07:10:49 -07:00
rav4kumar ce092ddfa8 rip vibecontroller in to two halfs 2026-01-23 07:10:49 -07:00
rav4kumar 895d6a925e abh, bsm 2026-01-23 07:10:49 -07: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
Jason Wen e5e56614c9 ui: Customizable Interactive Timeout (#1640)
* ui: Custom Interactive Timeout

* rename

* lint
2026-01-04 00:33:32 -05:00
Nayan 1eb82fcc85 ui: Global Brightness Override (#1579)
* global brightness

* initialize

* keep stock

* lint

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-01-04 00:27:22 -05:00
Copilot 987f53e69a [TIZI/TICI] ui: sunnylink status on sidebar (#1638)
* Initial plan

* feat: add sunnylink status metric

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

* chore: extract sidebar constants

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

* refactor: guard metric spacing

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

* chore: clarify sunnylink helpers

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

* refactor: guard metric spacing edge cases

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

* chore: simplify spacing guards

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

* chore: normalize sunnylink params

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

* chore: harden sunnylink param parsing

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

* chore: add param decode helper

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

* chore: simplify sidebar metric spacing

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

* chore: update sunnylink status color logic for improved clarity

* sunnylink: update status handling to reflect offline state and improve fault indication

sunnylink: enhance status handling with temporary fault indication

* sunnylink: enhance status update logic for improved accuracy and clarity

* make it int

* Ugly with zero value, but done. Now we only need to remember to check the new sidebar if the old sidebar ever changes

* Revert "Ugly with zero value, but done. Now we only need to remember to check the new sidebar if the old sidebar ever changes"

This reverts commit 2d3b740e38.

* decouple

* no bad bot

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com>
Co-authored-by: DevTekVE <devtekve@gmail.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-01-03 21:01:21 -05:00
github-actions[bot] 9a04a5eaae [bot] Update Python packages (#1565)
* Update Python packages

* no

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-01-03 15:54:55 -05:00
Nayan 50b8ae9e09 sunnylink: update params metadata (#1636)
* sunnylink model controls

* cleanup more controls

* update verbiage

Co-authored-by: DevTekVE <devtekve@gmail.com>

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
2026-01-03 08:35:02 -05: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
James Vecellio-Grant a30fc9bcd2 modeld: configurable camera offset (#1614)
* 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.

* modeld: camera offset class

* verify zero offset I @ A = A

* slithered and slunked

* Update params_metadata.json

* wait

* Update model_renderer.py

* Update model_renderer.py

* requested changes

* stricter

* Update model_renderer.py

* more

* return default

* Update params_metadata.json

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-01-01 14:23:01 -05: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
Kumar 19a7d1d5d7 [TIZI/TICI] ui: update dmoji position and Developer UI adjustments (#1601)
* ui: improve layout and centering of bottom developer UI elements

* int

* less is more, y'all

* always show actual lat for all cars

* lint

* perfect

* cleanup

* too long

* inherit

* remove unused

* inir

* need to fix

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-31 00:27:53 -05:00
Jason Wen fb8f46cba9 Reimplement sunnypilot Terms of Service & sunnylink Consent Screens (#1633)
* tos reimpl

* nah

* simpler

* check consent on sunnylink panel - mici

* slight cleanup

* rename

* keep it off

* decouple

* more rename

* more decouple

* a bit more

* fix state

* decouple more

* a bit more

* wrong type

* rearrange

* don't do that

* final

* lint

* include

* more

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
2025-12-31 00:08:36 -05:00
Nayan 70386c6b00 ui: fix Always Offroad button visibility (#1632)
always offroad button fix
2025-12-30 23:20:20 -05:00
James Vecellio-Grant edeede5e82 modeld_v2: conditional model compilation for metadrive testing (#1623)
* modeld_v2: conditional model compilation for PC

* full send

* shebang

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-29 22:03:08 -05:00
Adeeb Shihadeh a5348b8679 crcmod -> crcmod-plus (#36968) 2025-12-29 16:41:52 -08:00
James Vecellio-Grant 6df313b974 modeld_v2: remove dead test (#1621)
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-29 17:01:58 -05:00
James Vecellio-Grant 9442bc9aec modeld_v2: planplus model tuning (#1620)
* modeld: planplus model tuning

* little more

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-29 16:55:39 -05: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
DevTekVE 763049f068 SL: Re enable and validate ingestion of swaglogs (#1580)
* Re enable and validate ingestion
2025-12-27 11:49:26 -05:00
Trey Moen 3f1f7ad89c feat(esim): remove bootstrap (#36954)
feat: remove bootstrap
2025-12-24 09:04:34 -08:00
Nayan c6c644a3a6 [mici] ui: sunnypilot font on home menu (#1561)
use sp font on home

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-23 16:34:00 -05:00
dzid26 c876a83a31 ui: fix malformed dongle ID display on the PC if dongleID is not set (#1612) 2025-12-23 09:34:56 -05:00
Nayan a04a5b4284 ui: expand DeviceLayoutSP (#1560)
* commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* sunnypilot: remove Qt

* cabana: revert to stock Qt

* commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9

* commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e

* commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e

* sum more

* bump opendbc

* not yet

* should've been symlink'ed

* raylib says wut

* quiet mode back

* more fixes

* no more

* too extra red diff on the side

* need to bring this back

* too extra

* let's update docs here

* Revert "let's update docs here"

This reverts commit 51fe03cd51.

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* multi-button

* Lint

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* sp raylib preview

* fix callback

* fix ui preview

* better padding

* this

* support for next line multi-button

* uhh

* disabled colors

* listitem -> listitemsp

* listitem -> listitemsp

* add show_description method

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

* ui: `GuiApplicationExt`

* simple button

* simple button

* add to readme

* use gui_app.sunnypilot_ui()

* i've got something to confessa

* init

* more init

* power buttons always visible

* uh, nope

* add reset to offroad only

* support wake up offroad

* flippity floppity

* dual button item sp

* use dual button item sp

* lint

* keep @devtekve from going blind

* more round

* some

* revert

* slight diff

* should've been inline

* cleanup power btns and offroad transitions

* bruh

* 1st row red diff

* 2nd row red diff

* 3rd row red diff

* slight diff

* move around

* more diff

* only when onroad we move to the top, not the toggle

* nah

* sort

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: DevTekVE <devtekve@gmail.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Co-authored-by: discountchubbs <alexgrant990@gmail.com>
2025-12-23 00:51:35 -05:00
Matt Purnell 373894a81f docs: Update branch installation instructions in README (#1610)
* Update branch installation instructions

* Add another link

* Add limited support link

* Incorporate suggested link

* Tweak wording
2025-12-23 00:42:24 -05:00
Jason Wen 34ce746869 ui: DualButtonActionSP and dual_button_item_sp helpers (#1608) 2025-12-22 09:29:01 -05:00
Matt Purnell e96b0da9d7 ci: Add unit test to prevent MADS DM regressions (#1602)
* monitoring/tests: Add unit test to prevent MADS regressions

* dm: remove TODO

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-21 23:07:01 -05:00
Jason Wen 6d7910ed74 ui: add inline to option_item_sp (#1600) 2025-12-21 22:56:51 -05:00
Jason Wen e54ddf30b8 ci: restore workflows from source branch for reset and squash (#1607) 2025-12-21 22:48:45 -05:00
Nayan 3093bb0b66 ui: fix sunnylink paired/sponsorship state (#1603)
* fix

* no point polling when disabled
2025-12-21 22:26:11 -05: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
eFini c3143f3833 Multilang: update zh translation (#36933) 2025-12-19 09:38:23 -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
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
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
252 changed files with 7677 additions and 6429 deletions
@@ -174,6 +174,24 @@ jobs:
echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig
echo ' locksverify = false' >> .lfsconfig
- name: Restore workflows from source
run: |
TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"
SOURCE_BRANCH="${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}"
# Ensure we are on the target branch
git checkout $TARGET_BRANCH
echo "Restoring .github/workflows from $SOURCE_BRANCH"
git checkout origin/$SOURCE_BRANCH -- .github/workflows
if ! git diff --cached --quiet; then
echo "Workflows differ. Committing restoration."
git commit -m "chore: restore .github/workflows from $SOURCE_BRANCH"
else
echo "Workflows match $SOURCE_BRANCH."
fi
- uses: actions/create-github-app-token@v2
id: ci-token
with:
-1
View File
@@ -108,7 +108,6 @@ jobs:
build_mac:
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' }}
if: false # There'll be one day that this works. That day is not today.
steps:
- uses: actions/checkout@v4
with:
+1
View File
@@ -4,6 +4,7 @@
[submodule "opendbc"]
path = opendbc_repo
url = https://github.com/sunnypilot/opendbc.git
branch = tn
[submodule "msgq"]
path = msgq_repo
url = https://github.com/commaai/msgq.git
Vendored
+1 -1
View File
@@ -22,7 +22,7 @@ shopt -s huponexit # kill all child processes when the shell exits
export CI=1
export PYTHONWARNINGS=error
export LOGPRINT=debug
#export LOGPRINT=debug # this has gotten too spammy...
export TEST_DIR=${env.TEST_DIR}
export SOURCE_DIR=${env.SOURCE_DIR}
export GIT_BRANCH=${env.GIT_BRANCH}
+2 -58
View File
@@ -11,66 +11,10 @@ Join the official sunnypilot community forum to stay up to date with all the lat
https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot
## 🚘 Running on a dedicated device in a car
* A supported device to run this software
* a [comma three](https://comma.ai/shop/products/three) or a [C3X](https://comma.ai/shop/comma-3x)
* This software
* One of [the 325+ supported cars](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford, and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car
Detailed instructions for [how to mount the device in a car](https://comma.ai/setup).
First, check out this list of items you'll need to [get started](https://community.sunnypilot.ai/t/getting-started-using-sunnypilot-in-your-supported-car/251).
## Installation
Please refer to [Recommended Branches](#recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging` branch.
### If you want to use our newest branches (our rewrite)
> [!TIP]
>You can see the rewrite state on our [rewrite project board](https://github.com/orgs/sunnypilot/projects/2), and to install the new branches, you can use the following links
* sunnypilot not installed or you installed a version before 0.8.17?
1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed.
2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option.
3. Input the installation URL per [Recommended Branches](#recommended-branches). Example: ```https://staging.sunnypilot.ai```.
4. Complete the rest of the installation following the onscreen instructions.
* sunnypilot already installed and you installed a version after 0.8.17?
1. On the comma three/3X, go to `Settings` ▶️ `Software`.
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot.
3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector.
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging`
### Recommended Branches
| Branch | Installation URL |
|:---------------:|:---------------------------------------------:|
| `release` | `https://release.sunnypilot.ai` |
| `staging` | `https://staging.sunnypilot.ai` |
| `dev` | `https://dev.sunnypilot.ai` |
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
> [!TIP]
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging'.
> [!NOTE]
> Do you require further assistance with software installation? Join the [sunnypilot community forum](https://community.sunnypilot.ai/new-topic?category=general/qa) and create a topic in the General/Q&A Category channel.
<details>
<summary>Older legacy branches</summary>
### If you want to use our older legacy branches (*not recommended*)
> [**IMPORTANT**]
> It is recommended to [re-flash AGNOS](https://flash.comma.ai/) if you intend to downgrade from the new branches.
> You can still restore the latest sunnylink backup made on the old branches.
| Branch | Installation URL |
|:------------:|:--------------------------------:|
| `release-c3` | https://release-c3.sunnypilot.ai |
| `staging-c3` | https://staging-c3.sunnypilot.ai |
| `dev-c3` | https://dev-c3.sunnypilot.ai |
</details>
Next, refer to the sunnypilot community forum for [installation instructions](https://community.sunnypilot.ai/t/read-before-installing-sunnypilot/254), as well as a complete list of [Recommended Branch Installations](https://community.sunnypilot.ai/t/recommended-branch-installations/235).
## 🎆 Pull Requests
We welcome both pull requests and issues on GitHub. Bug fixes are encouraged.
+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)
========================
* New driving model #36249
+127 -4
View File
@@ -192,6 +192,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
aTarget @5 :Float32;
events @6 :List(OnroadEventSP.Event);
e2eAlerts @7 :E2eAlerts;
accelPersonality @8 :AccelerationPersonality;
struct DynamicExperimentalControl {
state @0 :DynamicExperimentalControlState;
@@ -203,7 +204,11 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
blended @1;
}
}
enum AccelerationPersonality {
sport @0;
normal @1;
eco @2;
}
struct SmartCruiseControl {
vision @0 :Vision;
map @1 :Map;
@@ -288,6 +293,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
sccVision @1;
sccMap @2;
speedLimitAssist @3;
mapd @4;
}
struct E2eAlerts {
@@ -340,6 +346,7 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitChanged @21;
speedLimitPending @22;
e2eChime @23;
laneChangeRoadEdge @24;
}
}
@@ -446,6 +453,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
leftLaneChangeEdgeBlock @1 :Bool;
rightLaneChangeEdgeBlock @2 :Bool;
enum TurnDirection {
none @0;
@@ -475,11 +484,125 @@ struct CustomReserved15 @0xbd443b539493bc68 {
struct CustomReserved16 @0xfc6241ed8877b611 {
}
struct CustomReserved17 @0xa30662f84033036c {
struct MapdDownloadLocationDetails @0xff889853e7b0987f {
location @0 :Text;
totalFiles @1 :UInt32;
downloadedFiles @2 :UInt32;
}
struct CustomReserved18 @0xc86a3d38d13eb3ef {
struct MapdDownloadProgress @0xfaa35dcac85073a2 {
active @0 :Bool;
cancelled @1 :Bool;
totalFiles @2 :UInt32;
downloadedFiles @3 :UInt32;
locations @4 :List(Text);
locationDetails @5 :List(MapdDownloadLocationDetails);
}
struct CustomReserved19 @0xa4f1eb3323f5f582 {
struct MapdPathPoint @0xd6f78acca1bc3939 {
latitude @0 :Float64;
longitude @1 :Float64;
curvature @2 :Float32;
targetVelocity @3 :Float32;
}
struct MapdExtendedOut @0xa30662f84033036c {
downloadProgress @0 :MapdDownloadProgress;
settings @1 :Text;
path @2 :List(MapdPathPoint);
}
enum MapdInputType {
download @0;
setTargetLateralAccel @1;
setSpeedLimitOffset @2;
setSpeedLimitControl @3;
setMapCurveSpeedControl @4;
setVisionCurveSpeedControl @5;
setLogLevel @6;
setVisionCurveTargetLatA @7;
setVisionCurveMinTargetV @8;
reloadSettings @9;
saveSettings @10;
setEnableSpeed @11;
setVisionCurveUseEnableSpeed @12;
setMapCurveUseEnableSpeed @13;
setSpeedLimitUseEnableSpeed @14;
setHoldLastSeenSpeedLimit @15;
setTargetSpeedJerk @16;
setTargetSpeedAccel @17;
setTargetSpeedTimeOffset @18;
setDefaultLaneWidth @19;
setMapCurveTargetLatA @20;
loadDefaultSettings @21;
loadRecommendedSettings @22;
setSlowDownForNextSpeedLimit @23;
setSpeedUpForNextSpeedLimit @24;
setHoldSpeedLimitWhileChangingSetSpeed @25;
loadPersistentSettings @26;
cancelDownload @27;
setLogJson @28;
setLogSource @29;
setExternalSpeedLimitControl @30;
setExternalSpeedLimit @31;
setSpeedLimitPriority @32;
setSpeedLimitChangeRequiresAccept @33;
acceptSpeedLimit @34;
setPressGasToAcceptSpeedLimit @35;
setAdjustSetSpeedToAcceptSpeedLimit @36;
setAcceptSpeedLimitTimeout @37;
setPressGasToOverrideSpeedLimit @38;
}
enum WaySelectionType {
current @0;
predicted @1;
possible @2;
extended @3;
fail @4;
}
enum SpeedLimitOffsetType {
static @0;
percent @1;
}
struct MapdIn @0xc86a3d38d13eb3ef {
type @0 :MapdInputType;
float @1 :Float32;
str @2 :Text;
bool @3 :Bool;
}
enum RoadContext {
freeway @0;
city @1;
unknown @2;
}
struct MapdOut @0xa4f1eb3323f5f582 {
wayName @0 :Text;
wayRef @1 :Text;
roadName @2 :Text;
speedLimit @3 :Float32;
nextSpeedLimit @4 :Float32;
nextSpeedLimitDistance @5 :Float32;
hazard @6 :Text;
nextHazard @7 :Text;
nextHazardDistance @8 :Float32;
advisorySpeed @9 :Float32;
nextAdvisorySpeed @10 :Float32;
nextAdvisorySpeedDistance @11 :Float32;
oneWay @12 :Bool;
lanes @13 :UInt8;
tileLoaded @14 :Bool;
speedLimitSuggestedSpeed @15 :Float32;
suggestedSpeed @16 :Float32;
estimatedRoadWidth @17 :Float32;
roadContext @18 :RoadContext;
distanceFromWayCenter @19 :Float32;
visionCurveSpeed @20 :Float32;
mapCurveSpeed @21 :Float32;
waySelectionType @22 :WaySelectionType;
speedLimitAccepted @23 :Bool;
}
+4 -3
View File
@@ -87,6 +87,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
laneChange @50;
lowMemory @51;
stockAeb @52;
stockLkas @98;
ldw @53;
carUnrecognized @54;
invalidLkasSetting @55;
@@ -2642,9 +2643,9 @@ struct Event {
customReserved14 @140 :Custom.CustomReserved14;
customReserved15 @141 :Custom.CustomReserved15;
customReserved16 @142 :Custom.CustomReserved16;
customReserved17 @143 :Custom.CustomReserved17;
customReserved18 @144 :Custom.CustomReserved18;
customReserved19 @145 :Custom.CustomReserved19;
mapdExtendedOut @143 :Custom.MapdExtendedOut;
mapdIn @144 :Custom.MapdIn;
mapdOut @145 :Custom.MapdOut;
# *********** legacy + deprecated ***********
model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated
+1 -1
View File
@@ -13,7 +13,7 @@ from typing import Optional, List, Union, Dict
from cereal import log
from cereal.services import SERVICE_LIST
from openpilot.common.util import MovingAverage
from openpilot.common.utils import MovingAverage
NO_TRAVERSAL_LIMIT = 2**64-1
+1 -1
View File
@@ -98,9 +98,9 @@ _services: dict[str, tuple] = {
"carParamsSP": (True, 0.02, 1),
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"liveMapDataSP": (True, 1., 1),
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
"liveLocationKalman": (True, 20.),
"mapdOut": (True, 20., 20, QueueSize.MEDIUM),
# debug
"uiDebug": (True, 0., 1),
+1 -6
View File
@@ -19,11 +19,6 @@ if GetOption('extras'):
# Cython bindings
params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11'])
SConscript([
'transformations/SConscript',
])
Import('transformations_python')
common_python = [params_python, transformations_python]
common_python = [params_python]
Export('common_python')
+2 -2
View File
@@ -18,8 +18,8 @@ class Api:
return self.service.get_token(payload_extra, expiry_hours)
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **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, session, **params)
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')
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 = {}
if access_token is not None:
headers['Authorization'] = "JWT " + access_token
@@ -59,7 +59,9 @@ class BaseApi:
version = self.remove_non_ascii_chars(get_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
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
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)
@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)
@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)
@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)
@cache
def get_origin(cwd: str = None) -> str:
def get_origin(cwd: str | None = None) -> str:
try:
local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"], 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
def get_normalized_origin(cwd: str = None) -> str:
def get_normalized_origin(cwd: str | None = None) -> str:
return get_origin(cwd) \
.replace("git@", "", 1) \
.replace(".git", "", 1) \
+1 -1
View File
@@ -1 +1 @@
#define DEFAULT_MODEL "Dark Souls 2 (Default)"
#define DEFAULT_MODEL "WMI (Default)"
+17 -33
View File
@@ -133,6 +133,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
@@ -145,15 +147,18 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CarParamsSPPersistent", {PERSISTENT, BYTES}},
{"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}},
{"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}},
{"CompletedSunnylinkConsentVersion", {PERSISTENT, STRING, "0"}},
{"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}},
{"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}},
{"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}},
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
{"DynamicFollow", {PERSISTENT | BACKUP, BOOL, "0"}},
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
{"HideVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
@@ -166,17 +171,23 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffControl", {PERSISTENT | BACKUP, BOOL}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
// toyota specific params
{"ToyotaAutoHold", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaTSS2Long", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaDriveMode", {PERSISTENT | BACKUP, BOOL, "0"}},
// MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
@@ -191,6 +202,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"MapdSettings", {PERSISTENT, JSON}},
{"Offroad_OSMUpdateRequired", {CLEAR_ON_MANAGER_START, JSON}},
// Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -219,43 +233,13 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
// sunnypilot model params
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
{"MapdVersion", {PERSISTENT, STRING}},
{"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}},
{"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_OSMUpdateRequired", {CLEAR_ON_MANAGER_START, JSON}},
{"OsmDbUpdatesCheck", {CLEAR_ON_MANAGER_START, BOOL}}, // mapd database update happens with device ON, reset on boot
{"OSMDownloadBounds", {PERSISTENT, STRING}},
{"OsmDownloadedDate", {PERSISTENT, STRING, "0.0"}},
{"OSMDownloadLocations", {PERSISTENT, JSON}},
{"OSMDownloadProgress", {CLEAR_ON_MANAGER_START, JSON}},
{"OsmLocal", {PERSISTENT, BOOL}},
{"OsmLocationName", {PERSISTENT, STRING}},
{"OsmLocationTitle", {PERSISTENT, STRING}},
{"OsmLocationUrl", {PERSISTENT, STRING}},
{"OsmStateName", {PERSISTENT, STRING, "All"}},
{"OsmStateTitle", {PERSISTENT, STRING}},
{"OsmWayTest", {PERSISTENT, STRING}},
{"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"RoadNameToggle", {PERSISTENT, STRING}},
// Speed Limit
{"SpeedLimitMode", {PERSISTENT | BACKUP, INT, "1"}},
{"SpeedLimitOffsetType", {PERSISTENT | BACKUP, INT, "0"}},
{"SpeedLimitPolicy", {PERSISTENT | BACKUP, INT, "3"}},
{"SpeedLimitValueOffset", {PERSISTENT | BACKUP, INT, "0"}},
// Smart Cruise Control
{"MapTargetVelocities", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"SmartCruiseControlMap", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SmartCruiseControlVision", {PERSISTENT | BACKUP, BOOL, "0"}},
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
// Torque lateral control custom params
{"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}},
+3 -9
View File
@@ -3,15 +3,9 @@ from numbers import Number
class PIDController:
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_i = k_i
self._k_d = 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._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p
self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i
self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d
self.set_limits(pos_limit, neg_limit)
+5 -1
View File
@@ -13,7 +13,11 @@ public:
if (prefix.empty()) {
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);
assert(ret);
setenv("OPENPILOT_PREFIX", prefix.c_str(), 1);
+4 -2
View File
@@ -1,4 +1,5 @@
import os
import platform
import shutil
import uuid
@@ -9,9 +10,10 @@ from openpilot.system.hardware.hw import Paths
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
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.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.clean_dirs_on_exit = clean_dirs_on_exit
self.shared_download_cache = shared_download_cache
+1 -1
View File
@@ -6,7 +6,7 @@ import time
from setproctitle import getproctitle
from openpilot.common.util import MovingAverage
from openpilot.common.utils import MovingAverage
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),
ecef_positions_offset_batch,
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 pytest
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
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(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)
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
from subprocess import Popen, PIPE, TimeoutExpired
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
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:
"""Wraps a file, but overrides the read method to also
call a callback function with the number of bytes read so far."""
def __init__(self, f, callback, *args):
self.f = f
self.callback = callback
@@ -107,11 +154,11 @@ def retry(attempts=3, delay=1.0, ignore_failure=False):
try:
return func(*args, **kwargs)
except Exception:
cloudlog.exception(f"{func.__name__} failed, trying again")
print(f"{func.__name__} failed, trying again")
time.sleep(delay)
if ignore_failure:
cloudlog.error(f"{func.__name__} failed after retry")
print(f"{func.__name__} failed after retry")
else:
raise Exception(f"{func.__name__} failed after retry")
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
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="15.1"
export AGNOS_VERSION="16"
fi
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
"requests", # many one-off uses
"sympy", # rednose + friends
"crcmod", # cars + qcomgpsd
"crcmod-plus", # cars + qcomgpsd
"tqdm", # cars (fw_versions.py) on start + many one-off uses
# hardwared
@@ -49,7 +49,7 @@ dependencies = [
# logging
"pyzmq",
"sentry-sdk",
"xattr", # used in place of 'os.getxattr' for macos compatibility
"xattr", # used in place of 'os.getxattr' for macOS compatibility
# athena
"PyJWT",
@@ -72,7 +72,7 @@ dependencies = [
"zstandard",
# 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",
"mapbox-earcut",
]
@@ -87,7 +87,7 @@ docs = [
testing = [
"coverage",
"hypothesis ==6.47.*",
"mypy",
"ty",
"pytest",
"pytest-cpp",
"pytest-subtests",
@@ -107,15 +107,13 @@ dev = [
"av",
"azure-identity",
"azure-storage-blob",
"dbus-next", # TODO: remove once we moved everything to jeepney
"dictdiffer",
"jeepney",
"matplotlib",
"opencv-python-headless",
"parameterized >=0.8, <0.9",
"pyautogui",
"pygame",
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
"pyopencl",
"pytools>=2025.1.6; platform_machine != 'aarch64'",
"pywinctl",
"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"
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
[tool.ruff]
indent-width = 2
@@ -275,3 +237,43 @@ lint.flake8-implicit-str-concat.allow-multiline = false
[tool.ruff.format]
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
### Go to staging
- [ ] make a GitHub issue to track release
- [ ] make a GitHub issue to track release with this checklist
- [ ] 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`
- [ ] build new userdata partition from `release3-staging`
- [ ] 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
- [ ] before going to release, test the following:
- [ ] update from previous release -> new release
@@ -26,7 +25,7 @@ Updating staging:
- [ ] check sentry, MTBF, etc.
- [ ] stress test passes in production
- [ ] 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`
- [ ] create GitHub release
- [ ] 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
if [[ -z "$FAST" ]]; then
run "mypy" mypy $PYTHON_FILES
run "ty" ty check
run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore
fi
@@ -69,7 +69,7 @@ function help() {
echo ""
echo -e "${BOLD}${UNDERLINE}Tests:${NC}"
echo -e " ${BOLD}ruff${NC}"
echo -e " ${BOLD}mypy${NC}"
echo -e " ${BOLD}ty${NC}"
echo -e " ${BOLD}codespell${NC}"
echo -e " ${BOLD}check_added_large_files${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 ""
echo -e "${BOLD}${UNDERLINE}Examples:${NC}"
echo " op lint mypy ruff"
echo " Only run the mypy and ruff tests"
echo " op lint ty ruff"
echo " Only run the ty and ruff tests"
echo ""
echo " op lint --skip mypy ruff"
echo " Skip the mypy and ruff tests"
echo " op lint --skip ty ruff"
echo " Skip the ty and ruff tests"
echo ""
echo " op lint"
echo " Run all the tests"
+19 -54
View File
@@ -1,7 +1,8 @@
from cereal import car, log, custom
import cereal.messaging as messaging
from cereal import car, log
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.toyota.values import ToyotaFlags
from openpilot.selfdrive.selfdrived.events import Events
@@ -11,33 +12,6 @@ EventName = log.OnroadEvent.EventName
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:
def __init__(self, CP: structs.CarParams):
self.CP = CP
@@ -48,14 +22,12 @@ class CarSpecificEvents:
self.silent_steer_warning = True
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'):
events = Events()
return Events()
elif self.CP.brand == 'chrysler':
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
events = self.create_common_events(CS, CS_prev)
if self.CP.brand == 'chrysler':
# Low speed steer alert hysteresis logic
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
self.low_speed_alert = True
@@ -65,8 +37,6 @@ class CarSpecificEvents:
events.add(EventName.belowSteerSpeed)
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:
events.add(EventName.belowEngageSpeed)
@@ -87,11 +57,9 @@ class CarSpecificEvents:
elif self.CP.brand == 'toyota':
# 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:
# 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)
if CS.vEgo < self.CP.minEnableSpeed:
events.add(EventName.belowEngageSpeed)
@@ -103,8 +71,6 @@ class CarSpecificEvents:
events.add(EventName.manualRestart)
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
# 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
@@ -114,8 +80,6 @@ class CarSpecificEvents:
events.add(EventName.resumeRequired)
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 CS.vEgo < self.CP.minEnableSpeed + 0.5:
events.add(EventName.belowEngageSpeed)
@@ -123,27 +87,26 @@ class CarSpecificEvents:
events.add(EventName.speedTooLow)
# 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)
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
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True,
allow_button_cancel=True):
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState):
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:
events.add(EventName.doorOpen)
if CS.seatbeltUnlatched:
events.add(EventName.seatbeltNotLatched)
if CS.gearShifter != GearShifter.drive and (extra_gears is None or
CS.gearShifter not in extra_gears):
if CS.gearShifter != GearShifter.drive and CS.gearShifter not in CI.DRIVABLE_GEARS:
events.add(EventName.wrongGear)
if CS.gearShifter == GearShifter.reverse:
events.add(EventName.reverseGear)
@@ -157,6 +120,8 @@ class CarSpecificEvents:
events.add(EventName.stockFcw)
if CS.stockAeb:
events.add(EventName.stockAeb)
if CS.stockLkas:
events.add(EventName.stockLkas)
if CS.vEgo > MAX_CTRL_SPEED:
events.add(EventName.speedTooHigh)
if CS.cruiseState.nonAdaptive:
+8 -7
View File
@@ -10,7 +10,7 @@ from cereal import car, log, custom
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
from openpilot.common.swaglog import cloudlog, ForwardingHandler
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from opendbc.car import DT_CTRL, structs
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
from opendbc.car.carlog import carlog
@@ -19,7 +19,6 @@ from opendbc.car.car_helpers import get_car, interfaces
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
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.car_specific import MockCarState
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
@@ -123,7 +122,13 @@ class Car:
self.CI, self.CP, self.CP_SP = CI, CI.CP, CI.CP_SP
self.RI = RI
# set alternative experiences from parameters
sp_toyota_auto_brake_hold = self.params.get_bool("ToyotaAutoHold")
self.CP.alternativeExperience = 0
if sp_toyota_auto_brake_hold:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
# mads
set_alternative_experience(self.CP, self.CP_SP, self.params)
set_car_specific_params(self.CP, self.CP_SP, self.params)
@@ -139,7 +144,7 @@ class Car:
safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput
self.CP.safetyConfigs = [safety_config]
if self.CP.secOcRequired and not is_release:
if self.CP.secOcRequired:
# Copy user key if available
try:
with open("/cache/params/SecOCKey") as f:
@@ -179,7 +184,6 @@ class Car:
self.params.put_nonblocking("CarParamsSPCache", 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.is_metric = self.params.get_bool("IsMetric")
@@ -200,8 +204,6 @@ class Car:
# Update carState from CAN
CS, CS_SP = self.CI.update(can_list)
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
RD: structs.RadarDataT | None = self.RI.update(can_list)
@@ -217,7 +219,6 @@ class Car:
if can_rcv_valid and REPLAY:
self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime
self.v_cruise_helper.update_speed_limit_assist(self.is_metric, self.sm['longitudinalPlanSP'])
self.v_cruise_helper.update_v_cruise(CS, self.sm['carControl'].enabled, self.is_metric)
if self.sm['carControl'].enabled and not self.CC_prev.enabled:
# Use CarState w/ buttons from the step selfdrived enables on
-7
View File
@@ -53,7 +53,6 @@ class VCruiseHelper(VCruiseHelperSP):
if not self.CP.pcmCruise or (not self.CP_SP.pcmCruiseSpeed and _enabled):
# if stock cruise is completely disabled, then we can use our own set speed logic
self._update_v_cruise_non_pcm(CS, _enabled, is_metric)
self.update_speed_limit_assist_v_cruise_non_pcm()
self.v_cruise_cluster_kph = self.v_cruise_kph
self.update_button_timers(CS, enabled)
else:
@@ -105,12 +104,6 @@ class VCruiseHelper(VCruiseHelperSP):
if not self.button_change_states[button_type]["enabled"]:
return
# Speed Limit Assist for Non PCM long cars.
# True: Disallow set speed changes when user confirmed the target set speed during preActive state
# False: Allow set speed changes as SLA is not requesting user confirmation
if self.update_speed_limit_assist_pre_active_confirmed(button_type):
return
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta
+3 -3
View File
@@ -56,7 +56,7 @@ class DesireHelper:
def get_lane_change_direction(CS):
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
def update(self, carstate, lateral_active, lane_change_prob):
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected, right_edge_detected):
self.alc.update_params()
self.lane_turn_controller.update_params()
v_ego = carstate.vEgo
@@ -88,8 +88,8 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
@@ -327,8 +327,10 @@ class LongitudinalMpc:
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
return lead_xv
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
t_follow = get_T_FOLLOW(personality)
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard, a_cruise_min_override=None, t_follow_override=None):
t_follow = t_follow_override if t_follow_override is not None else get_T_FOLLOW(personality)
a_cruise_min = a_cruise_min_override if a_cruise_min_override is not None else CRUISE_MIN_ACCEL
v_ego = self.x0[1]
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
@@ -350,7 +352,7 @@ class LongitudinalMpc:
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
# when the leads are no factor.
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
v_lower = v_ego + (T_IDXS * a_cruise_min * 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),
@@ -124,7 +124,11 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
if mode == 'acc':
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
if sp_accel_clip := LongitudinalPlannerSP.get_accel_clip(self, v_ego, mode):
accel_clip = sp_accel_clip
else:
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
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)
else:
@@ -154,7 +158,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
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.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality)
a_cruise_min_override = LongitudinalPlannerSP.get_cruise_min_accel(self, v_ego)
t_follow_override = LongitudinalPlannerSP.get_t_follow(self, v_ego)
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality, a_cruise_min_override=a_cruise_min_override, t_follow_override=t_follow_override)
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)
+1 -2
View File
@@ -27,12 +27,11 @@ def main():
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
'liveMapDataSP', 'carStateSP', gps_location_service],
'carStateSP', 'mapdOut', gps_location_service],
poll='carState')
while True:
sm.update()
longitudinal_planner.sla.update_car_state(sm['carState'])
if sm.updated['modelV2']:
longitudinal_planner.update(sm)
longitudinal_planner.publish(sm, pm)
@@ -108,11 +108,11 @@ if __name__ == "__main__":
uds_client = UdsClient(panda, 0x7D0, bus=args.bus)
print("\n[START DIAGNOSTIC SESSION]")
session_type : SESSION_TYPE = 0x07 # type: ignore
session_type : SESSION_TYPE = 0x07
uds_client.diagnostic_session_control(session_type)
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)
print(fw_version)
if fw_version not in SUPPORTED_FW_VERSIONS.keys():
@@ -120,7 +120,7 @@ if __name__ == "__main__":
sys.exit(1)
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)
config_values = SUPPORTED_FW_VERSIONS[fw_version]
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")
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')
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()
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:]
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
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):
print("Security access failed!")
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)
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(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):
print("Writing new configuration failed!")
print("Make sure the comma processes are stopped: tmux kill-session -t comma")
@@ -156,7 +156,7 @@ if __name__ == "__main__":
try:
# 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}")
except (NegativeResponseError, MessageTimeoutError):
print("Reading back updated coding failed!")
-1
View File
@@ -1,5 +1,4 @@
#!/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.
Features:
+2 -4
View File
@@ -99,8 +99,7 @@ def cycle_alerts(duration=200, is_metric=False):
alert = AM.process_alerts(frame, [])
print(alert)
for _ in range(duration):
dat = messaging.new_message()
dat.init('selfdriveState')
dat = messaging.new_message('selfdriveState')
dat.selfdriveState.enabled = False
if alert:
@@ -112,8 +111,7 @@ def cycle_alerts(duration=200, is_metric=False):
dat.selfdriveState.alertSound = alert.audible_alert
pm.send('selfdriveState', dat)
dat = messaging.new_message()
dat.init('deviceState')
dat = messaging.new_message('deviceState')
dat.deviceState.started = True
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
# show FW fingerprint
print("\nFW fingerprint:\n")
for f in fw:
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
print(f" {f.fwVersion},")
print(" ],")
if fw:
print("\nFW fingerprint:\n")
for f in fw:
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
print(f" {f.fwVersion},")
print(" ],")
print()
print(f"VIN: {vin}")
-1
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# type: ignore
import random
from collections import defaultdict
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# type: ignore
import os
import argparse
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# type: ignore
from collections import defaultdict
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:
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:
@@ -92,7 +92,7 @@ class Calibrator:
valid_blocks: int = 0,
wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_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():
self.rpy = RPY_INIT.copy()
else:
+1 -1
View File
@@ -94,7 +94,7 @@ class PointBuckets:
def add_point(self, x: float, y: float) -> None:
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()])
if num_points is None:
return points
+2 -2
View File
@@ -127,8 +127,8 @@ class VehicleParamsLearner:
if not self.active:
# Reset time when stopped so uncertainty doesn't grow
self.kf.filter.set_filter_time(t) # type: ignore
self.kf.filter.reset_rewind() # type: ignore
self.kf.filter.set_filter_time(t)
self.kf.filter.reset_rewind()
def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder:
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
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):
Executable
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
import os
import glob
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc')
lenv = env.Clone()
lenvCython = envCython.Clone()
+6 -2
View File
@@ -33,7 +33,7 @@ from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -298,6 +298,7 @@ def main(demo=False):
prev_action = log.ModelDataV2.Action()
DH = DesireHelper()
RELC = RoadEdgeLaneChangeController(params.get_bool("RoadEdgeLaneChangeEnabled"))
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -395,7 +396,10 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs)
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
+1 -1
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8fe9a71b0fd428a045a82ed50790179f77aa664391198f078e11e7b2cb2c2d7
oid sha256:1edea5bb56f876db4cec97c150799513f6a59373f3ad152d55e4dcaab1b809e3
size 13926324
-1
View File
@@ -449,7 +449,6 @@ class DriverMonitoring:
rpyCalib = [0., 0., 0.]
else:
highway_speed = sm['carState'].vEgo
# TODO-SP: unit test to assert both control checks are always present
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
standstill = sm['carState'].standstill
+65 -1
View File
@@ -1,6 +1,7 @@
import numpy as np
import pytest
from cereal import log
from cereal import log, car
from openpilot.common.realtime import DT_DMON
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
from openpilot.system.hardware import HARDWARE
@@ -204,3 +205,66 @@ class TestMonitoring:
assert EventName.driverUnresponsive in \
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
(False, False, False), # Both Disabled
(True, False, True), # OP Enabled, Lat Inactive
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
(True, True, True) # Both Active
])
def test_enabled_states(enabled_state, lat_active_state, expected):
"""
Test DriverMonitoring.run_step with all 4 combinations of:
- selfdriveState.enabled (True/False)
- carControl.latActive (True/False)
"""
cs = car.CarState.new_message()
cs.vEgo = 30.0
cs.gearShifter = car.CarState.GearShifter.drive
cs.standstill = False
cs.steeringPressed = False
cs.gasPressed = False
ss = log.SelfdriveState.new_message()
ss.enabled = enabled_state
cc = car.CarControl.new_message()
cc.latActive = lat_active_state
mv2 = log.ModelDataV2.new_message()
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
lc = log.LiveCalibrationData.new_message()
lc.rpyCalib = [0.0, 0.0, 0.0]
ds = make_msg(False)
sm = {
'carState': cs,
'selfdriveState': ss,
'carControl': cc,
'modelV2': mv2,
'liveCalibration': lc,
'driverStateV2': ds
}
driver_monitoring = DriverMonitoring()
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
captured_args = []
original_update_events = driver_monitoring._update_events
def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
captured_args.append(op_engaged)
return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
driver_monitoring._update_events = spy_update_events
driver_monitoring.run_step(sm, demo=False)
# Assertion
assert len(captured_args) == 1, "Expected _update_events to be called exactly once"
actual_enabled = captured_args[0]
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"
+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)
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:
a = copy.copy(OFFROAD_ALERTS[alert])
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"),
},
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: {
ET.PERMANENT: Alert(
"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
# If you're not writing a new car port, this is usually cause by faulty wiring
EventName.canError: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Error"),
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Unknown Vehicle Variant"),
ET.PERMANENT: Alert(
"CAN Error: Check Connections",
"Unknown Vehicle Variant",
"",
AlertStatus.normal, AlertSize.small,
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: {
+11 -5
View File
@@ -233,8 +233,8 @@ class SelfdriveD(CruiseHelper):
# Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0
if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \
(CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
(CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
(CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
(CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
self.events.add(EventName.pedalPressed)
# Create events for temperature, disk space, and memory
@@ -295,16 +295,22 @@ class SelfdriveD(CruiseHelper):
# Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
direction = self.sm['modelV2'].meta.laneChangeDirection
mdv2sp = self.sm['modelDataV2SP']
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
(CS.rightBlindspot and direction == LaneChangeDirection.right):
(CS.rightBlindspot and direction == LaneChangeDirection.right):
self.events.add(EventName.laneChangeBlocked)
elif mdv2sp.leftLaneChangeEdgeBlock or mdv2sp.rightLaneChangeEdgeBlock:
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
else:
if direction == LaneChangeDirection.left:
self.events.add(EventName.preLaneChangeLeft)
else:
self.events.add(EventName.preLaneChangeRight)
elif self.sm['modelV2'].meta.laneChangeState in (LaneChangeState.laneChangeStarting,
LaneChangeState.laneChangeFinishing):
LaneChangeState.laneChangeFinishing):
self.events.add(EventName.laneChange)
# Handle lane turn
@@ -499,7 +505,7 @@ class SelfdriveD(CruiseHelper):
# All pandas not in silent mode must have controlsAllowed when openpilot is enabled
if self.enabled and any(not ps.controlsAllowed for ps in self.sm['pandaStates']
if ps.safetyModel not in IGNORED_SAFETY_MODES):
if ps.safetyModel not in IGNORED_SAFETY_MODES):
self.mismatch_counter += 1
return CS
+1 -1
View File
@@ -44,7 +44,7 @@ class FuzzyGenerator:
except capnp.lib.capnp.KjException:
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 ()
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')})
+2 -1
View File
@@ -1,5 +1,6 @@
from collections import defaultdict
from collections.abc import Callable
from typing import cast
import capnp
import functools
import traceback
@@ -69,7 +70,7 @@ def migrate(lr: LogIterable, migration_funcs: list[MigrationFunc]):
if migration.product in grouped: # skip if product already exists
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]
r_ops, a_ops, d_ops = migration(msg_gen)
replace_ops.extend(r_ops)
@@ -614,9 +614,9 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
def replay_process(
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None,
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None,
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None = 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 = None, disable_progress: bool = False
) -> list[capnp._DynamicStructReader]:
if isinstance(cfg, Iterable):
cfgs = list(cfg)
+1 -1
View File
@@ -16,7 +16,7 @@ from openpilot.tools.lib.openpilotci import get_url
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
) -> list[capnp._DynamicStructReader]:
all_msgs = sorted(lr, key=lambda m: m.logMonoTime)
+1 -1
View File
@@ -19,7 +19,7 @@ SOURCES: list[AzureContainer] = [
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:
exclude_patterns = [r'dcamera\.hevc']
+1 -1
View File
@@ -39,7 +39,7 @@ class HomeLayout(Widget):
self.current_state = HomeLayoutState.HOME
self.last_refresh = 0
self.settings_callback: callable | None = None
self.settings_callback: Callable[[], None] | None = None
self.update_available = False
self.alert_count = 0
+38 -9
View File
@@ -11,7 +11,9 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkOnboarding
DEBUG = False
@@ -33,6 +35,7 @@ class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class TrainingGuide(Widget):
@@ -110,14 +113,14 @@ class TermsPage(Widget):
self._on_decline = on_decline
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at https://comma.ai/terms before continuing."),
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
def _render(self, _):
welcome_x = self._rect.x + 165
welcome_x = self._rect.x + 95
welcome_y = self._rect.y + 165
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
self._title.render(welcome_rect)
@@ -143,7 +146,7 @@ class TermsPage(Widget):
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms and Conditions in order to use sunnypilot."),
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
@@ -180,9 +183,21 @@ class OnboardingWindow(Widget):
self._training_guide: TrainingGuide | None = None
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
# sunnylink consent pages
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
self._sunnylink = SunnylinkOnboarding()
if not self._accepted_terms:
self._state = OnboardingState.TERMS
elif not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self._state = OnboardingState.ONBOARDING
@property
def completed(self) -> bool:
return self._accepted_terms and self._training_done
return self._accepted_terms and self._sunnylink.completed and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
@@ -192,8 +207,12 @@ class OnboardingWindow(Widget):
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
if self._training_done:
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
if not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
gui_app.set_modal_overlay(None)
def _on_completed_training(self):
@@ -206,8 +225,18 @@ class OnboardingWindow(Widget):
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
if self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
self._sunnylink.render(self._rect)
if self._sunnylink.completed:
if not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
gui_app.set_modal_overlay(None)
elif self._state == OnboardingState.ONBOARDING:
if not self._training_done:
self._training_guide.render(self._rect)
else:
gui_app.set_modal_overlay(None)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
+3 -5
View File
@@ -145,20 +145,18 @@ class SettingsLayout(Widget):
if panel.instance:
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
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback:
self._close_callback()
return True
return
# Check navigation buttons
for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
self.set_current_panel(panel_type)
return True
return False
return
def set_current_panel(self, panel_type: PanelType):
if panel_type != self._current_panel:
+13 -2
View File
@@ -9,6 +9,8 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.layouts.sidebar import SidebarSP
SIDEBAR_WIDTH = 300
METRIC_HEIGHT = 126
METRIC_WIDTH = 240
@@ -62,9 +64,10 @@ class MetricData:
self.color = color
class Sidebar(Widget):
class Sidebar(Widget, SidebarSP):
def __init__(self):
super().__init__()
Widget.__init__(self)
SidebarSP.__init__(self)
self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0
@@ -112,6 +115,7 @@ class Sidebar(Widget):
self._update_temperature_status(device_state)
self._update_connection_status(device_state)
self._update_panda_status()
SidebarSP._update_sunnylink_status(self)
def _update_network_status(self, device_state):
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
@@ -200,6 +204,13 @@ class Sidebar(Widget):
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
def _draw_metrics(self, rect: rl.Rectangle):
if gui_app.sunnypilot_ui():
metrics, start_y, spacing = SidebarSP._draw_metrics_w_sunnylink(self, rect, self._temp_status, self._panda_status, self._connect_status)
for idx, metric in enumerate(metrics):
self._draw_metric(rect, metric, start_y + idx * spacing)
return
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
for metric, y_offset in metrics:
+3 -1
View File
@@ -1,5 +1,6 @@
from enum import IntEnum
import os
import requests
import threading
import time
@@ -29,6 +30,7 @@ class PrimeState:
def __init__(self):
self._params = Params()
self._lock = threading.Lock()
self._session = requests.Session() # reuse session to reduce SSL handshake overhead
self.prime_type: PrimeType = self._load_initial_state()
self._running = False
@@ -50,7 +52,7 @@ class PrimeState:
try:
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:
data = response.json()
is_paired = data.get("is_paired", False)
+1 -1
View File
@@ -109,7 +109,7 @@ class MiciHomeLayout(Widget):
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
self._openpilot_label = MiciLabel("sunnypilot", font_size=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE)
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
+20 -8
View File
@@ -4,6 +4,7 @@ import cereal.messaging as messaging
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
from openpilot.selfdrive.ui.mici.layouts.mapd_panel import MapdInfoPanel
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
@@ -40,17 +41,19 @@ class MiciMainLayout(Widget):
self._alerts_layout = MiciOffroadAlerts()
self._settings_layout = SettingsLayout()
self._onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._mapd_panel = MapdInfoPanel()
# Initialize widget rects
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout):
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout, self._mapd_panel):
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._scroller = Scroller([
self._alerts_layout,
self._home_layout,
self._onroad_layout,
], spacing=0, pad_start=0, pad_end=0)
self._alerts_layout,
self._home_layout,
self._mapd_panel,
self._onroad_layout,
], spacing=0, pad_start=0, pad_end=0)
self._scroller.set_reset_scroll_at_show(False)
# Disable scrolling when onroad is interacting with bookmark
@@ -107,6 +110,14 @@ class MiciMainLayout(Widget):
self._layouts[mode].show_event()
self._current_mode = mode
def _is_on_side_panel(self) -> bool:
onroad_x = self._onroad_layout.rect.x
current_scroll = self._scroller.scroll_panel.get_offset()
return abs(current_scroll - onroad_x) > self._rect.width / 2
def _is_on_mapd_panel(self) -> bool:
return self._is_on_side_panel()
def _handle_transitions(self):
if ui_state.started != self._prev_onroad:
self._prev_onroad = ui_state.started
@@ -123,15 +134,16 @@ class MiciMainLayout(Widget):
CS = ui_state.sm["carState"]
if not CS.standstill and self._prev_standstill:
self._set_mode(MainState.MAIN)
self._scroll_to(self._onroad_layout)
if not self._is_on_mapd_panel():
self._set_mode(MainState.MAIN)
self._scroll_to(self._onroad_layout)
self._prev_standstill = CS.standstill
def _set_mode_for_started(self, onroad_transition: bool = False):
if ui_state.started:
CS = ui_state.sm["carState"]
# Only go onroad if car starts or is not at a standstill
if not CS.standstill or onroad_transition:
if (not CS.standstill or onroad_transition) and not self._is_on_mapd_panel():
self._set_mode(MainState.MAIN)
self._scroll_to(self._onroad_layout)
else:
+217
View File
@@ -0,0 +1,217 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
METER_TO_KM = 0.001
METER_TO_MILE = 0.000621371
@dataclass(frozen=True)
class MapdPanelColors:
white: rl.Color = rl.WHITE
black: rl.Color = rl.BLACK
red: rl.Color = rl.Color(255, 0, 0, 255)
green: rl.Color = rl.Color(0, 255, 0, 255)
grey: rl.Color = rl.Color(145, 155, 149, 241)
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
dark_grey: rl.Color = rl.Color(100, 100, 100, 255)
bg_dark: rl.Color = rl.Color(30, 30, 30, 255)
card_bg: rl.Color = rl.Color(50, 50, 50, 200)
COLORS = MapdPanelColors()
class MapdInfoPanel(Widget):
def __init__(self):
super().__init__()
self.speed_limit: float = 0.0
self.speed_limit_valid: bool = False
self.next_speed_limit: float = 0.0
self.next_speed_limit_distance: float = 0.0
self.road_name: str = ""
self.current_speed: float = 0.0
self.lanes: int = 0
self.road_context: str = ""
self.hazard: str = ""
self.vision_curve_speed: float = 0.0
self.map_curve_speed: float = 0.0
self.tile_loaded: bool = False
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
def _update_state(self) -> None:
sm = ui_state.sm
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
if sm.valid["mapdOut"]:
mapd = sm["mapdOut"]
self.speed_limit = mapd.speedLimit * speed_conv
self.speed_limit_valid = mapd.speedLimit > 0
self.next_speed_limit = mapd.nextSpeedLimit * speed_conv
self.next_speed_limit_distance = mapd.nextSpeedLimitDistance
self.road_name = mapd.roadName
self.lanes = mapd.lanes
self.hazard = mapd.hazard
self.vision_curve_speed = mapd.visionCurveSpeed * speed_conv
self.map_curve_speed = mapd.mapCurveSpeed * speed_conv
self.tile_loaded = mapd.tileLoaded
road_ctx = mapd.roadContext
if hasattr(road_ctx, 'raw'):
ctx_val = road_ctx.raw
else:
ctx_val = int(road_ctx)
self.road_context = ["Freeway", "City", "Unknown"][ctx_val] if ctx_val < 3 else "Unknown"
else:
self.speed_limit_valid = False
if sm.updated["carState"]:
self.current_speed = sm["carState"].vEgo * speed_conv
def _render(self, rect: rl.Rectangle) -> None:
self._update_state()
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
margin = 15
left_width = 130
right_x = rect.x + left_width + margin + 10
right_width = rect.width - left_width - margin * 3
sign_x = rect.x + margin + 5
sign_y = rect.y + (rect.height - 140) / 2
self._draw_speed_limit_sign(sign_x, sign_y)
info_y = rect.y + 25
# Road name
self._draw_road_name(right_x, info_y, right_width)
info_y += 55
# Road context (Freeway/City) and lanes
self._draw_road_details(right_x, info_y, right_width)
info_y += 45
# Next speed limit
self._draw_next_speed_limit(right_x, info_y, right_width)
info_y += 45
# Curve speeds
self._draw_curve_speeds(right_x, info_y, right_width)
info_y += 45
# Hazard warning Beta
if self.hazard:
self._draw_hazard(right_x, info_y, right_width)
status_text = tr("Map Loaded") if self.tile_loaded else tr("No Map Data")
status_color = COLORS.green if self.tile_loaded else COLORS.red
status_size = measure_text_cached(self._font_medium, status_text, 18)
status_pos = rl.Vector2(rect.x + (rect.width - status_size.x) / 2, rect.y + rect.height - 30)
rl.draw_text_ex(self._font_medium, status_text, status_pos, 18, 0, status_color)
def _draw_speed_limit_sign(self, x: float, y: float) -> None:
sign_width = 110 if ui_state.is_metric else 100
sign_height = 120 if ui_state.is_metric else 110
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid else "--"
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
if ui_state.is_metric:
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color)
else:
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color)
def _draw_road_name(self, x: float, y: float, width: float) -> None:
road_display = self.road_name if self.road_name else "--"
road_size = measure_text_cached(self._font_semi_bold, road_display, 34)
if road_size.x > width:
road_display = road_display[:20] + "..."
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), 34, 0, COLORS.white)
def _draw_road_details(self, x: float, y: float, width: float) -> None:
details = []
if self.road_context and self.road_context != "Unknown":
details.append(self.road_context)
if self.lanes > 0:
details.append(f"{self.lanes} {tr('lanes')}")
details_text = "".join(details) if details else "--"
rl.draw_text_ex(self._font_medium, details_text, rl.Vector2(x, y), 26, 0, COLORS.light_grey)
def _draw_next_speed_limit(self, x: float, y: float, width: float) -> None:
if self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit:
next_text = f"{tr('Next')}: {round(self.next_speed_limit)} ({self._format_distance(self.next_speed_limit_distance)})"
else:
next_text = f"{tr('Next')}: --"
rl.draw_text_ex(self._font_medium, next_text, rl.Vector2(x, y), 26, 0, COLORS.light_grey)
def _draw_curve_speeds(self, x: float, y: float, width: float) -> None:
unit = tr("km/h") if ui_state.is_metric else tr("mph")
vision_val = str(round(self.vision_curve_speed)) if self.vision_curve_speed > 0 else "--"
map_val = str(round(self.map_curve_speed)) if self.map_curve_speed > 0 else "--"
curve_text = f"{tr('Curve')}: V:{vision_val} M:{map_val} {unit}"
rl.draw_text_ex(self._font_medium, curve_text, rl.Vector2(x, y), 24, 0, COLORS.light_grey)
def _draw_hazard(self, x: float, y: float, width: float) -> None:
hazard_text = f"{self.hazard}"
rl.draw_text_ex(self._font_semi_bold, hazard_text, rl.Vector2(x, y), 26, 0, COLORS.red)
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color) -> None:
center = rl.Vector2(x + width / 2, y + height / 2)
outer_radius = min(width, height) / 2
rl.draw_circle_v(center, outer_radius, COLORS.white)
ring_width = outer_radius * 0.18
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color) -> None:
sign_rect = rl.Rectangle(x, y, width, height)
rl.draw_rectangle_rounded(sign_rect, 0.2, 10, COLORS.white)
rl.draw_rectangle_rounded_lines_ex(sign_rect, 0.2, 10, 3, COLORS.black)
speed_label = tr("SPEED")
limit_label = tr("LIMIT")
label_size = 16
speed_text_size = measure_text_cached(self._font_semi_bold, speed_label, label_size)
speed_pos = rl.Vector2(x + (width - speed_text_size.x) / 2, y + 8)
rl.draw_text_ex(self._font_semi_bold, speed_label, speed_pos, label_size, 0, COLORS.black)
limit_text_size = measure_text_cached(self._font_semi_bold, limit_label, label_size)
limit_pos = rl.Vector2(x + (width - limit_text_size.x) / 2, y + 26)
rl.draw_text_ex(self._font_semi_bold, limit_label, limit_pos, label_size, 0, COLORS.black)
font_size = 40 if len(speed_str) >= 3 else 50
num_size = measure_text_cached(self._font_bold, speed_str, font_size)
num_pos = rl.Vector2(x + (width - num_size.x) / 2, y + 45)
rl.draw_text_ex(self._font_bold, speed_str, num_pos, font_size, 0, speed_color)
def _format_distance(self, distance: float) -> str:
if ui_state.is_metric:
if distance < 50:
return tr("Near")
if distance >= 1000:
return f"{distance * METER_TO_KM:.1f}" + tr("km")
if distance < 200:
rounded = max(10, int(distance / 10) * 10)
else:
rounded = int(distance / 100) * 100
return str(rounded) + tr("m")
else:
distance_mi = distance * METER_TO_MILE
if distance_mi < 0.1:
return tr("Near")
return f"{distance_mi:.1f}" + tr("mi")
+53 -21
View File
@@ -17,13 +17,16 @@ from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class DriverCameraSetupDialog(DriverCameraDialog):
@@ -166,8 +169,8 @@ class TrainingGuideDMTutorial(Widget):
def _update_state(self):
super()._update_state()
if device.awake:
ui_state.params.put_bool("IsDriverViewEnabled", True)
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
sm = ui_state.sm
if sm.recv_frame.get("driverMonitoringState", 0) == 0:
@@ -237,19 +240,20 @@ class TrainingGuideDMTutorial(Widget):
ring_color,
)
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
if self._dialog._camera_view.frame:
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
self._good_button.render(rl.Rectangle(
self._rect.x + self._rect.width - self._good_button.rect.width - 8,
self._rect.y + self._rect.height - self._good_button.rect.height,
self._good_button.rect.width,
self._good_button.rect.height,
))
self._good_button.render(rl.Rectangle(
self._rect.x + self._rect.width - self._good_button.rect.width - 8,
self._rect.y + self._rect.height - self._good_button.rect.height,
self._good_button.rect.width,
self._good_button.rect.height,
))
# rounded border
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
@@ -412,10 +416,10 @@ class TermsPage(SetupTermsPage):
super().__init__(on_accept, on_decline, "decline")
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
self._title_header = TermsHeader("terms & conditions", info_txt)
self._title_header = TermsHeader("terms of service", info_txt)
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use sunnypilot. " +
"Read the latest terms at https://sunnypilot.ai/terms before continuing.", 36,
FontWeight.ROMAN)
@property
@@ -449,6 +453,18 @@ class OnboardingWindow(Widget):
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
# sunnylink consent pages
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
self._sunnylink = SunnylinkOnboarding()
if not self._accepted_terms:
self._state = OnboardingState.TERMS
elif not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self._state = OnboardingState.ONBOARDING
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
@@ -459,7 +475,7 @@ class OnboardingWindow(Widget):
@property
def completed(self) -> bool:
return self._accepted_terms and self._training_done
return self._accepted_terms and self._sunnylink.completed and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
@@ -473,7 +489,13 @@ class OnboardingWindow(Widget):
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
if not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self.close()
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
@@ -482,8 +504,18 @@ class OnboardingWindow(Widget):
def _render(self, _):
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
self._sunnylink.render(self._rect)
if self._sunnylink.completed:
if not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self.close()
elif self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
if not self._training_done:
self._training_guide.render(self._rect)
else:
self.close()
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
@@ -1,3 +1,4 @@
import requests
import threading
import time
import pyray as rl
@@ -44,6 +45,7 @@ class FirehoseLayoutBase(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._session = requests.Session() # reuse session to reduce SSL handshake overhead
self._segment_count = self._get_segment_count()
self._scroll_panel = GuiScrollPanel2(horizontal=False)
@@ -203,7 +205,7 @@ class FirehoseLayoutBase(Widget):
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
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:
data = response.json()
self._segment_count = data.get("firehose", 0)
@@ -36,8 +36,6 @@ class WifiIcon(Widget):
super().__init__()
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_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)
@@ -57,17 +55,13 @@ class WifiIcon(Widget):
return
# Determine which wifi strength icon to use
strength = round(self._network.strength / 100 * 4)
if strength == 4:
strength = round(self._network.strength / 100 * 2)
if strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 3:
elif strength == 1:
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:
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_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2)
@@ -388,7 +382,7 @@ class WifiUIMici(BigMultiOptionDialog):
else:
network_button = WifiItem(network)
self.add_button(network_button)
self._scroller.add_widget(network_button)
# remove networks no longer present
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._update_buttons()
def _on_option_selected(self, option: str, smooth_scroll: bool = True):
super()._on_option_selected(option, smooth_scroll)
def _on_option_selected(self, option: str):
super()._on_option_selected(option)
# only open if button is already selected
if option in self._networks and option == self._selected_option:
if option in self._networks:
self._network_info_page.set_current_network(self._networks[option])
self._open_network_manage_page()
@@ -453,7 +446,7 @@ class WifiUIMici(BigMultiOptionDialog):
current_selection = self.get_selected_option()
if self._restore_selection and current_selection in self._networks:
self._scroller._layout()
BigMultiOptionDialog._on_option_selected(self, current_selection, smooth_scroll=False)
BigMultiOptionDialog._on_option_selected(self, current_selection)
self._restore_selection = None
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._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 still animating out, keep the previous alert
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 enum import IntEnum
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
@@ -351,6 +354,14 @@ class AugmentedRoadView(CameraView):
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__":
gui_app.init_window("OnRoad Camera View")
+66 -1
View File
@@ -121,12 +121,17 @@ class HudRenderer(Widget):
self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50)
self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50)
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 44, 44)
self._txt_blind_spot_left: rl.Texture = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 108, 128)
self._txt_blind_spot_right: rl.Texture = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 108, 128)
self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._blind_spot_left_alpha_filter = FirstOrderFilter(0, 0.15, 1 / gui_app.target_fps)
self._blind_spot_right_alpha_filter = FirstOrderFilter(0, 0.15, 1 / gui_app.target_fps)
def set_wheel_critical_icon(self, critical: bool):
"""Set the wheel icon to critical or normal state."""
self._show_wheel_critical = critical
@@ -175,19 +180,79 @@ class HudRenderer(Widget):
if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState':
self._torque_bar.render(rect)
draw_set_speed = False
if self.is_cruise_set:
alpha = self._set_speed_alpha_filter.update(
0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
self._can_draw_top_icons and self._engaged
)
draw_set_speed = alpha >= 1e-2
if draw_set_speed:
self._draw_set_speed(rect)
self._draw_steering_wheel(rect)
self._draw_blind_spot_indicators(rect)
def _draw_blind_spot_indicators(self, rect: rl.Rectangle) -> None:
sm = ui_state.sm
car_state = sm['carState']
try:
left_detected = car_state.leftBlindspot
except AttributeError:
left_detected = False
try:
right_detected = car_state.rightBlindspot
except AttributeError:
right_detected = False
self._blind_spot_left_alpha_filter.update(1.0 if left_detected else 0.0)
self._blind_spot_right_alpha_filter.update(1.0 if right_detected else 0.0)
BLIND_SPOT_MARGIN_X = 20 # Distance from edge of screen
BLIND_SPOT_Y_OFFSET = 100 # Distance from top of screen
if self._blind_spot_left_alpha_filter.x > 0.01:
pos_x = int(rect.x + BLIND_SPOT_MARGIN_X)
pos_y = int(rect.y + BLIND_SPOT_Y_OFFSET)
alpha = int(255 * self._blind_spot_left_alpha_filter.x)
color = rl.Color(255, 255, 255, alpha)
rl.draw_texture(self._txt_blind_spot_left, pos_x, pos_y, color)
if self._blind_spot_right_alpha_filter.x > 0.01:
pos_x = int(rect.x + rect.width - BLIND_SPOT_MARGIN_X - self._txt_blind_spot_right.width)
pos_y = int(rect.y + BLIND_SPOT_Y_OFFSET)
alpha = int(255 * self._blind_spot_right_alpha_filter.x)
color = rl.Color(255, 255, 255, alpha)
rl.draw_texture(self._txt_blind_spot_right, pos_x, pos_y, color)
def _has_blind_spot_detected(self) -> bool:
car_state = ui_state.sm['carState']
try:
left_detected = car_state.leftBlindspot
except AttributeError:
left_detected = False
try:
right_detected = car_state.rightBlindspot
except AttributeError:
right_detected = False
return left_detected or right_detected
def _draw_steering_wheel(self, rect: rl.Rectangle) -> None:
wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel
has_blind_spot = self._has_blind_spot_detected()
if self._show_wheel_critical:
self._wheel_alpha_filter.update(255)
self._wheel_y_filter.update(0)
else:
if ui_state.status == UIStatus.DISENGAGED:
if ui_state.status == UIStatus.DISENGAGED or has_blind_spot:
self._wheel_alpha_filter.update(0)
self._wheel_y_filter.update(wheel_txt.height / 2)
else:
+11 -4
View File
@@ -80,6 +80,9 @@ class ModelRenderer(Widget):
self._transform_dirty = True
self._clip_region = None
self._counter = -1
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
self._exp_gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
@@ -99,6 +102,10 @@ class ModelRenderer(Widget):
def _render(self, rect: rl.Rectangle):
sm = ui_state.sm
if self._counter % 180 == 0: # This runs at 60fps, so we query every 3 seconds
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
self._counter += 1
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
# Check if data is up-to-date
@@ -150,13 +157,13 @@ class ModelRenderer(Widget):
def _update_raw_points(self, model):
"""Update raw 3D points from model data"""
self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
for i, lane_line in enumerate(model.laneLines):
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
for i, road_edge in enumerate(model.roadEdges):
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
@@ -174,7 +181,7 @@ class ModelRenderer(Widget):
# Get z-coordinate from path at the lead vehicle position
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z)
if point:
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
+177
View File
@@ -0,0 +1,177 @@
import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
METER_TO_KM = 0.001
METER_TO_MILE = 0.000621371
SPEED_LIMIT_WIDTH_METRIC = 200
SPEED_LIMIT_WIDTH_IMPERIAL = 172
SPEED_LIMIT_HEIGHT = 204
@dataclass(frozen=True)
class SpeedLimitColors:
white: rl.Color = rl.WHITE
black: rl.Color = rl.BLACK
red: rl.Color = rl.Color(255, 0, 0, 255)
grey: rl.Color = rl.Color(145, 155, 149, 241)
green: rl.Color = rl.Color(0, 255, 0, 255)
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
dark_grey: rl.Color = rl.Color(180, 180, 180, 255)
border_grey: rl.Color = rl.Color(77, 77, 77, 255)
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
COLORS = SpeedLimitColors()
class SpeedLimitRenderer(Widget):
def __init__(self):
super().__init__()
self.speed_limit: float = 0.0
self.speed_limit_valid: bool = False
self.next_speed_limit: float = 0.0
self.next_speed_limit_distance: float = 0.0
self.road_name: str = ""
self.current_speed: float = 0.0
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
def _update_state(self) -> None:
sm = ui_state.sm
if sm.recv_frame["carState"] < ui_state.started_frame:
return
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
if sm.valid["mapdOut"]:
mapd = sm["mapdOut"]
self.speed_limit = mapd.speedLimit * speed_conv
self.speed_limit_valid = mapd.speedLimit > 0
self.next_speed_limit = mapd.nextSpeedLimit * speed_conv
self.next_speed_limit_distance = mapd.nextSpeedLimitDistance
self.road_name = mapd.roadName
else:
self.speed_limit_valid = False
if sm.updated["carState"]:
self.current_speed = sm["carState"].vEgo * speed_conv
def _render(self, rect: rl.Rectangle) -> None:
self._update_state()
if not self.speed_limit_valid:
return
sign_width = SPEED_LIMIT_WIDTH_METRIC if ui_state.is_metric else SPEED_LIMIT_WIDTH_IMPERIAL
sign_height = SPEED_LIMIT_HEIGHT
sign_margin = 12
sign_x = rect.x + sign_margin
sign_y = rect.y + 45
sign_rect = rl.Rectangle(sign_x, sign_y, sign_width, sign_height)
speed_str = str(round(self.speed_limit))
speed_color = COLORS.white if self.current_speed <= self.speed_limit else COLORS.red
if ui_state.is_metric:
self._draw_vienna_sign(sign_rect, speed_str, "", speed_color, True)
else:
self._draw_mutcd_sign(sign_rect, speed_str, "", speed_color, True)
if self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit:
self._draw_upcoming_speed_limit(sign_rect)
def _draw_vienna_sign(self, sign_rect: rl.Rectangle, speed_str: str, sub_text: str, speed_color: rl.Color, has_speed_limit: bool) -> None:
"""Draw EU/Vienna sign."""
center = rl.Vector2(sign_rect.x + sign_rect.width / 2, sign_rect.y + sign_rect.height / 2)
outer_radius = min(sign_rect.width, sign_rect.height) / 2
circle_size = outer_radius * 2
rl.draw_circle_v(center, outer_radius, COLORS.white)
ring_width = outer_radius * 0.18
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
font_size = circle_size * (0.35 if len(speed_str) >= 3 else 0.45)
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
def _draw_mutcd_sign(self, sign_rect: rl.Rectangle, speed_str: str, sub_text: str, speed_color: rl.Color, has_speed_limit: bool) -> None:
"""Draw US/Canada MUTCD sign."""
padding = 8
inner_rect = rl.Rectangle(sign_rect.x + padding, sign_rect.y + padding, sign_rect.width - (padding * 2), sign_rect.height - (padding * 2))
rl.draw_rectangle_rounded(inner_rect, 0.35, 10, COLORS.white)
rl.draw_rectangle_rounded_lines_ex(inner_rect, 0.35, 10, 4, COLORS.black)
border_width = 4
border_rect = rl.Rectangle(
inner_rect.x + border_width, inner_rect.y + border_width, inner_rect.width - (border_width * 2), inner_rect.height - (border_width * 2)
)
speed_text = tr("SPEED")
limit_text = tr("LIMIT")
speed_size = measure_text_cached(self._font_semi_bold, speed_text, 40)
limit_size = measure_text_cached(self._font_semi_bold, limit_text, 40)
speed_pos = rl.Vector2(border_rect.x + (border_rect.width - speed_size.x) / 2, sign_rect.y + 27)
limit_pos = rl.Vector2(border_rect.x + (border_rect.width - limit_size.x) / 2, sign_rect.y + 65)
rl.draw_text_ex(self._font_semi_bold, speed_text, speed_pos, 40, 0, COLORS.black)
rl.draw_text_ex(self._font_semi_bold, limit_text, limit_pos, 40, 0, COLORS.black)
font_size = 70 if len(speed_str) >= 3 else 90
speed_value_size = measure_text_cached(self._font_bold, speed_str, font_size)
speed_value_pos = rl.Vector2(border_rect.x + (border_rect.width - speed_value_size.x) / 2, sign_rect.y + 100)
rl.draw_text_ex(self._font_bold, speed_str, speed_value_pos, font_size, 0, speed_color)
def _draw_upcoming_speed_limit(self, sign_rect: rl.Rectangle) -> None:
"""Draw upcoming speed limit."""
speed_str = str(round(self.next_speed_limit))
distance_str = self._format_distance(self.next_speed_limit_distance)
ahead_width = 150
ahead_height = 100
ahead_x = sign_rect.x + (sign_rect.width - ahead_width) / 2
ahead_y = sign_rect.y + sign_rect.height + 6
ahead_rect = rl.Rectangle(ahead_x, ahead_y, ahead_width, ahead_height)
rl.draw_rectangle_rounded(ahead_rect, 0.2, 10, rl.Color(0, 0, 0, 180))
rl.draw_rectangle_rounded_lines_ex(ahead_rect, 0.2, 10, 3, rl.Color(255, 255, 255, 100))
ahead_label = tr("AHEAD")
ahead_size = measure_text_cached(self._font_semi_bold, ahead_label, 28)
ahead_pos = rl.Vector2(ahead_rect.x + (ahead_rect.width - ahead_size.x) / 2, ahead_rect.y + 4)
rl.draw_text_ex(self._font_semi_bold, ahead_label, ahead_pos, 28, 0, COLORS.light_grey)
speed_size = measure_text_cached(self._font_bold, speed_str, 48)
speed_pos = rl.Vector2(ahead_rect.x + (ahead_rect.width - speed_size.x) / 2, ahead_rect.y + 26)
rl.draw_text_ex(self._font_bold, speed_str, speed_pos, 48, 0, COLORS.white)
distance_size = measure_text_cached(self._font_medium, distance_str, 28)
distance_pos = rl.Vector2(ahead_rect.x + (ahead_rect.width - distance_size.x) / 2, ahead_rect.y + 70)
rl.draw_text_ex(self._font_medium, distance_str, distance_pos, 28, 0, COLORS.dark_grey)
def _format_distance(self, distance: float) -> str:
if ui_state.is_metric:
if distance < 50:
return tr("Near")
if distance >= 1000:
return f"{distance * METER_TO_KM:.1f}" + tr("km")
if distance < 200:
rounded = max(10, int(distance / 10) * 10)
else:
rounded = int(distance / 100) * 100
return str(rounded) + tr("m")
else:
distance_mi = distance * METER_TO_MILE
if distance_mi < 0.1:
return tr("Near")
return f"{distance_mi:.1f}" + tr("mi")
+8 -8
View File
@@ -71,7 +71,7 @@ class BigCircleButton(Widget):
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)
self._toggle_callback = toggle_callback
@@ -251,7 +251,7 @@ class BigButton(Widget):
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, "")
self._checked = initial_state
self._toggle_callback = toggle_callback
@@ -288,8 +288,8 @@ class BigToggle(BigButton):
class BigMultiToggle(BigToggle):
def __init__(self, text: str, options: list[str], toggle_callback: Callable = None,
select_callback: Callable = None):
def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable | None = None):
super().__init__(text, "", toggle_callback=toggle_callback)
assert len(options) > 0
self._options = options
@@ -327,8 +327,8 @@ class BigMultiToggle(BigToggle):
class BigMultiParamToggle(BigMultiToggle):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None,
select_callback: Callable = None):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable | None = None):
super().__init__(text, options, toggle_callback, select_callback)
self._param = param
@@ -345,7 +345,7 @@ class BigMultiParamToggle(BigMultiToggle):
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)
self.param = param
self.params = Params()
@@ -361,7 +361,7 @@ class BigParamControl(BigToggle):
# TODO: param control base class
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)
self._param = param
self.params = Params()
+38 -17
View File
@@ -4,17 +4,17 @@ import pyray as rl
from typing import Union
from collections.abc import Callable
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.label import UnifiedLabel, gui_label
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.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.slider import RedBigSlider, BigSlider
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
DEBUG = False
@@ -137,7 +137,7 @@ class BigInputDialog(BigDialogBase):
hint: str,
default_text: str = "",
minimum_length: int = 1,
confirm_callback: Callable[[str], None] = None):
confirm_callback: Callable[[str], None] | 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)),
font_weight=FontWeight.MEDIUM)
@@ -317,39 +317,36 @@ class BigMultiOptionDialog(BigDialogBase):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
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)
self._options = options
if default is not None:
assert default in options
self._default_option: str = default or (options[0] if len(options) > 0 else "")
self._selected_option: str = self._default_option
self._default_option: str | None = default
self._selected_option: str = self._default_option or (options[0] if len(options) > 0 else "")
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)
if self._right_btn is not None:
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
for option in options:
self.add_button(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)
self._scroller.add_widget(BigDialogOptionButton(option))
def show_event(self):
super().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:
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
for btn in self._scroller._items:
btn = cast(BigDialogOptionButton, btn)
@@ -365,11 +362,35 @@ class BigMultiOptionDialog(BigDialogBase):
y_pos = rect_center_y - (btn.rect.y + height / 2)
break
self._scroller.scroll_to(-y_pos, smooth=smooth_scroll)
self._scroller.scroll_to(-y_pos)
def _selected_option_changed(self):
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):
super()._update_state()
+4
View File
@@ -116,6 +116,10 @@ class AlertRenderer(Widget):
def _render(self, rect: rl.Rectangle):
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:
return
+11 -2
View File
@@ -15,9 +15,10 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera
from openpilot.common.transformations.orientation import rot_from_euler
if gui_app.sunnypilot_ui():
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.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
CALIBRATED = log.LiveCalibrationData.Status.calibrated
@@ -223,6 +224,14 @@ class AugmentedRoadView(CameraView):
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__":
gui_app.init_window("OnRoad Camera View")
+1 -6
View File
@@ -50,12 +50,7 @@ class ExpButton(Widget):
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
src_rect = rl.Rectangle(0.0, 0.0, texture.width, texture.height)
dest_rect = rl.Rectangle(center_x, center_y, texture.width, texture.height)
origin = rl.Vector2(texture.width / 2.0, texture.height / 2.0)
rotation = -ui_state.sm['carState'].steeringAngleDeg
rl.draw_texture_pro(texture, src_rect, dest_rect, origin, rotation, self._white_color)
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
def _held_or_actual_mode(self):
now = time.monotonic()
+4
View File
@@ -2,6 +2,7 @@ import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
from openpilot.selfdrive.ui.onroad.speed_limit_ui import SpeedLimitRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
@@ -71,6 +72,7 @@ class HudRenderer(Widget):
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
self._exp_button: ExpButton = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
self._speed_limit_renderer: SpeedLimitRenderer = SpeedLimitRenderer()
def _update_state(self) -> None:
"""Update HUD state based on car state and controls state."""
@@ -112,6 +114,8 @@ class HudRenderer(Widget):
COLORS.HEADER_GRADIENT_END,
)
self._speed_limit_renderer.render(rect)
if self.is_cruise_available:
self._draw_set_speed(rect)
+10 -5
View File
@@ -56,7 +56,8 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
self._road_edge_stds = np.zeros(2, dtype=np.float32)
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
self._path_offset_z = HEIGHT_INIT[0]
self._counter = -1
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
# Initialize ModelPoints objects
self._path = ModelPoints()
self._lane_lines = [ModelPoints() for _ in range(4)]
@@ -103,6 +104,10 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
live_calib = sm['liveCalibration']
self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0]
if self._counter % 60 == 0:
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
self._counter += 1
if sm.updated['carParams']:
self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl
@@ -136,13 +141,13 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
def _update_raw_points(self, model):
"""Update raw 3D points from model data"""
self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
for i, lane_line in enumerate(model.laneLines):
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
for i, road_edge in enumerate(model.roadEdges):
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
@@ -160,7 +165,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
# Get z-coordinate from path at the lead vehicle position
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z)
if point:
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
+235
View File
@@ -0,0 +1,235 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
METER_TO_KM = 0.001
METER_TO_MILE = 0.000621371
@dataclass(frozen=True)
class SpeedLimitColors:
white: rl.Color = rl.WHITE
black: rl.Color = rl.BLACK
red: rl.Color = rl.Color(255, 0, 0, 255)
green: rl.Color = rl.Color(0, 255, 0, 255)
grey: rl.Color = rl.Color(145, 155, 149, 241)
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
dark_grey: rl.Color = rl.Color(100, 100, 100, 255)
bg_dark: rl.Color = rl.Color(20, 20, 20, 230)
card_bg: rl.Color = rl.Color(50, 50, 50, 200)
COLORS = SpeedLimitColors()
class SpeedLimitRenderer(Widget):
def __init__(self):
super().__init__()
self.speed_limit: float = 0.0
self.speed_limit_valid: bool = False
self.next_speed_limit: float = 0.0
self.next_speed_limit_distance: float = 0.0
self.road_name: str = ""
self.current_speed: float = 0.0
self.lanes: int = 0
self.road_context: str = ""
self.hazard: str = ""
self.vision_curve_speed: float = 0.0
self.map_curve_speed: float = 0.0
self.tile_loaded: bool = False
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
def _update_state(self) -> None:
sm = ui_state.sm
if sm.recv_frame["carState"] < ui_state.started_frame:
return
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
if sm.valid["mapdOut"]:
mapd = sm["mapdOut"]
self.speed_limit = mapd.speedLimit * speed_conv
self.speed_limit_valid = mapd.speedLimit > 0
self.next_speed_limit = mapd.nextSpeedLimit * speed_conv
self.next_speed_limit_distance = mapd.nextSpeedLimitDistance
self.road_name = mapd.roadName
self.lanes = mapd.lanes
self.hazard = mapd.hazard
self.vision_curve_speed = mapd.visionCurveSpeed * speed_conv
self.map_curve_speed = mapd.mapCurveSpeed * speed_conv
self.tile_loaded = mapd.tileLoaded
road_ctx = mapd.roadContext
if hasattr(road_ctx, 'raw'):
ctx_val = road_ctx.raw
else:
ctx_val = int(road_ctx)
self.road_context = ["Freeway", "City", "Unknown"][ctx_val] if ctx_val < 3 else "Unknown"
else:
self.speed_limit_valid = False
self.tile_loaded = False
if sm.updated["carState"]:
self.current_speed = sm["carState"].vEgo * speed_conv
def _render(self, rect: rl.Rectangle) -> None:
self._update_state()
panel_width = 420
panel_height = 280
panel_margin = 20
panel_x = rect.x + panel_margin
panel_y = rect.y + (rect.height - panel_height) / 2
panel_rect = rl.Rectangle(panel_x, panel_y, panel_width, panel_height)
rl.draw_rectangle_rounded(panel_rect, 0.08, 10, COLORS.bg_dark)
rl.draw_rectangle_rounded_lines_ex(panel_rect, 0.08, 10, 2, rl.Color(80, 80, 80, 200))
padding = 20
sign_width = 140 if ui_state.is_metric else 130
sign_height = 150 if ui_state.is_metric else 140
sign_x = panel_x + padding
sign_y = panel_y + padding
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
info_x = sign_x + sign_width + 20
info_width = panel_width - sign_width - padding * 2 - 20
info_y = panel_y + padding + 5
# Road name
self._draw_road_name(info_x, info_y, info_width)
info_y += 50
# Road context and lanes
self._draw_road_details(info_x, info_y, info_width)
info_y += 40
# Next speed limit
self._draw_next_speed_limit(info_x, info_y, info_width)
info_y += 40
# Curve speeds
self._draw_curve_speeds(info_x, info_y, info_width)
# Hazard warning beta
if self.hazard:
hazard_y = panel_y + panel_height - 60
self._draw_hazard(panel_x + padding, hazard_y, panel_width - padding * 2)
status_text = tr("Map Loaded") if self.tile_loaded else tr("No Map Data")
status_color = COLORS.green if self.tile_loaded else COLORS.red
status_size = measure_text_cached(self._font_medium, status_text, 22)
status_pos = rl.Vector2(panel_x + (panel_width - status_size.x) / 2, panel_y + panel_height - 30)
rl.draw_text_ex(self._font_medium, status_text, status_pos, 22, 0, status_color)
def _draw_speed_limit_sign(self, x: float, y: float, width: float, height: float) -> None:
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid else "--"
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
if ui_state.is_metric:
self._draw_vienna_sign(x, y, width, height, speed_str, speed_color)
else:
self._draw_mutcd_sign(x, y, width, height, speed_str, speed_color)
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color) -> None:
center = rl.Vector2(x + width / 2, y + height / 2)
outer_radius = min(width, height) / 2
rl.draw_circle_v(center, outer_radius, COLORS.white)
ring_width = outer_radius * 0.18
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
font_size = outer_radius * (0.75 if len(speed_str) >= 3 else 0.95)
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color) -> None:
sign_rect = rl.Rectangle(x, y, width, height)
rl.draw_rectangle_rounded(sign_rect, 0.2, 10, COLORS.white)
rl.draw_rectangle_rounded_lines_ex(sign_rect, 0.2, 10, 4, COLORS.black)
speed_label = tr("SPEED")
limit_label = tr("LIMIT")
label_size = 22
speed_text_size = measure_text_cached(self._font_semi_bold, speed_label, label_size)
speed_pos = rl.Vector2(x + (width - speed_text_size.x) / 2, y + 12)
rl.draw_text_ex(self._font_semi_bold, speed_label, speed_pos, label_size, 0, COLORS.black)
limit_text_size = measure_text_cached(self._font_semi_bold, limit_label, label_size)
limit_pos = rl.Vector2(x + (width - limit_text_size.x) / 2, y + 36)
rl.draw_text_ex(self._font_semi_bold, limit_label, limit_pos, label_size, 0, COLORS.black)
font_size = 55 if len(speed_str) >= 3 else 70
num_size = measure_text_cached(self._font_bold, speed_str, font_size)
num_pos = rl.Vector2(x + (width - num_size.x) / 2, y + 65)
rl.draw_text_ex(self._font_bold, speed_str, num_pos, font_size, 0, speed_color)
def _draw_road_name(self, x: float, y: float, width: float) -> None:
road_display = self.road_name if self.road_name else "--"
font_size = 32
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
if road_size.x > width:
while len(road_display) > 3 and measure_text_cached(self._font_semi_bold, road_display + "...", font_size).x > width:
road_display = road_display[:-1]
road_display = road_display + "..."
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
def _draw_road_details(self, x: float, y: float, width: float) -> None:
details = []
if self.road_context and self.road_context != "Unknown":
details.append(self.road_context)
if self.lanes > 0:
details.append(f"{self.lanes} {tr('lanes')}")
details_text = "".join(details) if details else "--"
rl.draw_text_ex(self._font_medium, details_text, rl.Vector2(x, y), 26, 0, COLORS.light_grey)
def _draw_next_speed_limit(self, x: float, y: float, width: float) -> None:
if self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit:
next_text = f"{tr('Next')}: {round(self.next_speed_limit)} ({self._format_distance(self.next_speed_limit_distance)})"
else:
next_text = f"{tr('Next')}: --"
rl.draw_text_ex(self._font_medium, next_text, rl.Vector2(x, y), 26, 0, COLORS.light_grey)
def _draw_curve_speeds(self, x: float, y: float, width: float) -> None:
unit = tr("km/h") if ui_state.is_metric else tr("mph")
vision_val = str(round(self.vision_curve_speed)) if self.vision_curve_speed > 0 else "--"
map_val = str(round(self.map_curve_speed)) if self.map_curve_speed > 0 else "--"
curve_text = f"{tr('Curve')}: V:{vision_val} M:{map_val} {unit}"
rl.draw_text_ex(self._font_medium, curve_text, rl.Vector2(x, y), 24, 0, COLORS.light_grey)
def _draw_hazard(self, x: float, y: float, width: float) -> None:
hazard_text = f"{self.hazard}"
rl.draw_text_ex(self._font_semi_bold, hazard_text, rl.Vector2(x, y), 28, 0, COLORS.red)
def _format_distance(self, distance: float) -> str:
if ui_state.is_metric:
if distance < 50:
return tr("Near")
if distance >= 1000:
return f"{distance * METER_TO_KM:.1f}" + tr("km")
if distance < 200:
rounded = max(10, int(distance / 10) * 10)
else:
rounded = int(distance / 100) * 100
return str(rounded) + tr("m")
else:
distance_mi = distance * METER_TO_MILE
if distance_mi < 0.1:
return tr("Near")
return f"{distance_mi:.1f}" + tr("mi")
@@ -0,0 +1,116 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
class SunnylinkConsentPage(Widget):
def __init__(self, done_callback=None):
super().__init__()
self._done_callback = done_callback
self._step = 0
self._title = Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._content = [
{
"text": tr("sunnylink enables secured remote access to your comma device from anywhere, " +
"including settings management, remote monitoring, real-time dashboard, etc."),
"primary_btn": tr("Enable"),
"secondary_btn": tr("Disable"),
"highlight_primary": True
},
{
"text": tr("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
"If sunnylink is disabled, features such as settings management, remote monitoring, " +
"real-time dashboards will be unavailable."),
"secondary_btn": tr("Back"),
"danger_btn": tr("Disable"),
"highlight_primary": True
}
]
self._primary_btn = Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))
self._secondary_btn = Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))
self._danger_btn = Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))
def _handle_choice(self, choice):
if choice == "enable":
ui_state.params.put_bool("SunnylinkEnabled", True)
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
if self._done_callback:
self._done_callback()
elif choice == "secondary":
if self._step == 0:
self._step = 1
elif self._step == 1:
self._step = 0
elif choice == "disable":
ui_state.params.put_bool("SunnylinkEnabled", False)
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
if self._done_callback:
self._done_callback()
def _render(self, _):
step_data = self._content[self._step]
welcome_x = self._rect.x + 95
welcome_y = self._rect.y + 165
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
self._title.render(welcome_rect)
desc_x = welcome_x
desc_y = welcome_y + 120
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
desc_label = Label(step_data["text"], font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
desc_label.render(desc_rect)
btn_y = self._rect.y + self._rect.height - 160 - 45
if "danger_btn" in step_data:
btn_width = (self._rect.width - 45 * 3) / 2
self._secondary_btn.set_text(step_data["secondary_btn"])
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
self._danger_btn.set_text(step_data["danger_btn"])
self._danger_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
else:
btn_width = (self._rect.width - 45 * 3) / 2
self._secondary_btn.set_text(step_data["secondary_btn"])
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
self._primary_btn.set_text(step_data["primary_btn"])
self._primary_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
return -1
class SunnylinkOnboarding:
def __init__(self):
self.consent_page = SunnylinkConsentPage(done_callback=self._on_done)
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
@property
def completed(self) -> bool:
return self.consent_done
def _on_done(self):
self.consent_done = True
def render(self, rect):
if not self.consent_done:
self.consent_page.render(rect)
@@ -5,8 +5,216 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, multiple_button_item_sp, button_item_sp, \
dual_button_item_sp, Spacer
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import ButtonStyle
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
from openpilot.system.ui.widgets.list_view import text_item
from openpilot.system.ui.widgets.scroller_tici import LineSeparator
offroad_time_options = {
0: 0,
1: 5,
2: 10,
3: 15,
4: 30,
5: 60,
6: 120,
7: 180,
8: 300,
9: 600,
10: 1440,
11: 1800,
}
class DeviceLayoutSP(DeviceLayout):
def __init__(self):
DeviceLayout.__init__(self)
self._scroller._line_separator = None
def _initialize_items(self):
DeviceLayout._initialize_items(self)
# Using dual button with no right button for better alignment
self._always_offroad_btn = dual_button_item_sp(
left_text=lambda: tr("Enable Always Offroad"),
left_callback=self._handle_always_offroad,
right_text="",
right_callback=None,
)
self._always_offroad_btn.action_item.right_button.set_visible(False)
self._max_time_offroad = option_item_sp(
title=lambda: tr("Max Time Offroad"),
description=lambda: tr("Device will automatically shutdown after set time once the engine is turned off.\n(30h is the default)"),
param="MaxTimeOffroad",
min_value=0,
max_value=11,
value_change_step=1,
on_value_changed=None,
enabled=True,
icon="",
value_map=offroad_time_options,
label_width=360,
use_float_scaling=False,
inline=True,
label_callback=self._update_max_time_offroad_label
)
self._device_wake_mode = multiple_button_item_sp(
title=lambda: tr("Wake Up Behavior"),
description=self.wake_mode_description,
param="DeviceBootMode",
buttons=[lambda: tr("Default"), lambda: tr("Offroad")],
button_width=364,
callback=None,
inline=True,
)
self._quiet_mode_and_dcam = dual_button_item_sp(
left_text=lambda: tr("Quiet Mode"),
right_text=lambda: tr("Driver Camera Preview"),
left_callback=lambda: ui_state.params.put_bool("QuietMode", not ui_state.params.get_bool("QuietMode")),
right_callback=self._show_driver_camera
)
self._quiet_mode_and_dcam.action_item.right_button.set_button_style(ButtonStyle.NORMAL)
self._reg_and_training = dual_button_item_sp(
left_text=lambda: tr("Regulatory"),
left_callback=self._on_regulatory,
right_text=lambda: tr("Training Guide"),
right_callback=self._on_review_training_guide
)
self._reg_and_training.action_item.right_button.set_button_style(ButtonStyle.NORMAL)
self._onroad_uploads_and_reset_settings = dual_button_item_sp(
left_text=lambda: tr("Onroad Uploads"),
left_callback=lambda: ui_state.params.put_bool("OnroadUploads", not ui_state.params.get_bool("OnroadUploads")),
right_text=lambda: tr("Reset Settings"),
right_callback=self._reset_settings
)
self._power_buttons = dual_button_item_sp(
left_text=lambda: tr("Reboot"),
right_text=lambda: tr("Power Off"),
left_callback=self._reboot_prompt,
right_callback=self._power_off_prompt
)
items = [
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
LineSeparator(),
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
LineSeparator(),
self._pair_device_btn,
LineSeparator(),
self._reset_calib_btn,
LineSeparator(),
button_item_sp(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
LineSeparator(),
self._device_wake_mode,
LineSeparator(),
self._max_time_offroad,
LineSeparator(height=10),
self._quiet_mode_and_dcam,
self._reg_and_training,
self._onroad_uploads_and_reset_settings,
Spacer(10),
LineSeparator(height=10),
self._power_buttons,
]
return items
def _offroad_transition(self):
self._power_buttons.action_item.right_button.set_visible(ui_state.is_offroad())
@staticmethod
def wake_mode_description() -> str:
def_str = tr("Default: Device will boot/wake-up normally & will be ready to engage.")
offrd_str = tr("Offroad: Device will be in Always Offroad mode after boot/wake-up.")
header = tr("Controls state of the device after boot/sleep.")
return f"{header}\n\n{def_str}\n{offrd_str}"
@staticmethod
def _reset_settings():
def _do_reset(result: int):
if result == DialogResult.CONFIRM:
for _key in ui_state.params.all_keys():
ui_state.params.remove(_key)
HARDWARE.reboot()
def _second_confirm(result: int):
if result == DialogResult.CONFIRM:
gui_app.set_modal_overlay(ConfirmDialog(
text=tr("The reset cannot be undone. You have been warned."),
confirm_text=tr("Confirm")
), callback=_do_reset)
gui_app.set_modal_overlay(ConfirmDialog(
text=tr("Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back."),
confirm_text=tr("Reset")
), callback=_second_confirm)
@staticmethod
def _handle_always_offroad():
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Enter Always Offroad Mode")))
return
_offroad_mode_state = ui_state.params.get_bool("OffroadMode")
_offroad_mode_str = tr("Are you sure you want to exit Always Offroad mode?") if _offroad_mode_state else \
tr("Are you sure you want to enter Always Offroad mode?")
def _set_always_offroad(result: int):
if result == DialogResult.CONFIRM and not ui_state.engaged:
ui_state.params.put_bool("OffroadMode", not _offroad_mode_state)
gui_app.set_modal_overlay(ConfirmDialog(_offroad_mode_str, tr("Confirm")), callback=lambda result: _set_always_offroad(result))
@staticmethod
def _update_max_time_offroad_label(value: int) -> str:
label = tr("Always On") if value == 0 else f"{value}" + tr("m") if value < 60 else f"{value // 60}" + tr("h")
label += tr(" (Default)") if value == 1800 else ""
return label
def _update_state(self):
super()._update_state()
# Handle Always Offroad button
always_offroad = ui_state.params.get_bool("OffroadMode")
# Text & Color
offroad_mode_btn_text = tr("Exit Always Offroad") if always_offroad else tr("Enable Always Offroad")
offroad_mode_btn_style = ButtonStyle.NORMAL if always_offroad else ButtonStyle.DANGER
self._always_offroad_btn.action_item.left_button.set_text(offroad_mode_btn_text)
self._always_offroad_btn.action_item.left_button.set_button_style(offroad_mode_btn_style)
# Position
if self._scroller._items.__contains__(self._always_offroad_btn):
self._scroller._items.remove(self._always_offroad_btn)
if ui_state.is_offroad() and not always_offroad:
self._scroller._items.insert(len(self._scroller._items) - 1, self._always_offroad_btn)
else:
self._scroller._items.insert(0, self._always_offroad_btn)
# Quiet Mode button
self._quiet_mode_and_dcam.action_item.left_button.set_button_style(ButtonStyle.PRIMARY if ui_state.params.get_bool("QuietMode") else ButtonStyle.NORMAL)
# Onroad Uploads
self._onroad_uploads_and_reset_settings.action_item.left_button.set_button_style(
ButtonStyle.PRIMARY if ui_state.params.get_bool("OnroadUploads") else ButtonStyle.NORMAL
)
# Offroad only buttons
self._quiet_mode_and_dcam.action_item.right_button.set_enabled(ui_state.is_offroad())
self._reg_and_training.action_item.left_button.set_enabled(ui_state.is_offroad())
self._reg_and_training.action_item.right_button.set_enabled(ui_state.is_offroad())
self._onroad_uploads_and_reset_settings.action_item.right_button.set_enabled(ui_state.is_offroad())
@@ -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.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
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.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):
@@ -18,11 +30,69 @@ class DisplayLayout(Widget):
self._scroller = Scroller(items, line_separator=True, spacing=0)
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 = [
self._onroad_brightness,
self._onroad_brightness_timer,
self._interactivity_timeout,
]
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):
self._scroller.render(rect)
@@ -1,232 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import datetime
import os
import platform
import requests
import shutil
import threading
from pathlib import Path
from time import monotonic
from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.layouts.settings.software import time_ago
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import text_item
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeFolder, TreeNode, TreeOptionDialog
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
MAP_PATH = Path(Paths.mapd_root()) / "offline"
class OSMLayout(Widget):
def __init__(self):
super().__init__()
self._current_percent = 0
self._last_map_size_update = 0
self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params
self._initialize_items()
self._update_map_size()
self._progress.set_visible(False)
self._state_btn.set_visible(False)
self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...")
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
def _initialize_items(self):
self._mapd_version = text_item(tr("Mapd Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...")
self._delete_maps_btn = ListItemSP(tr("Downloaded Maps"), action_item=NoElideButtonAction(tr("DELETE"), enabled=True), callback=self._delete_maps)
self._progress = progress_item(tr("Downloading Map"))
self._update_btn = ListItemSP(tr("Database Update"), action_item=NoElideButtonAction(tr("CHECK"), enabled=True), callback=self._update_db)
self._country_btn = ListItemSP(tr("Country"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("Country"))
self._state_btn = ListItemSP(tr("State"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("State"))
self.items = [self._mapd_version, self._delete_maps_btn, self._progress, self._update_btn, self._country_btn, self._state_btn]
def _show_confirm(self, msg, confirm_text, func):
gui_app.set_modal_overlay(ConfirmDialog(msg, confirm_text), lambda res: func() if res == DialogResult.CONFIRM else None)
def calculate_size(self):
total_size = 0
directories_to_scan = [MAP_PATH] if MAP_PATH.exists() else []
while directories_to_scan:
try:
for entry in os.scandir(directories_to_scan.pop()):
if entry.is_file():
total_size += entry.stat().st_size
elif entry.is_dir():
directories_to_scan.append(entry.path)
except OSError:
pass
self._delete_maps_btn.action_item.set_value(f"{total_size / 1024 ** 2:.2f} MB" if total_size < 1024 ** 3 else f"{total_size / 1024 ** 3:.2f} GB")
def _update_map_size(self):
threading.Thread(target=self.calculate_size, daemon=True).start()
def _do_delete_maps(self):
if MAP_PATH.exists():
shutil.rmtree(MAP_PATH)
for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"):
ui_state.params.remove(param)
self._delete_maps_btn.action_item.set_enabled(True)
self._delete_maps_btn.action_item.set_text(tr("DELETE"))
self._update_map_size()
def _on_confirm_delete_maps(self):
self._delete_maps_btn.action_item.set_enabled(False)
self._delete_maps_btn.action_item.set_text("DELETING...")
threading.Thread(target=self._do_delete_maps).start()
def _delete_maps(self):
self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
tr("Yes, delete all maps"), self._on_confirm_delete_maps)
def _update_db(self):
self._show_confirm(tr("This will start the download process and it might take a while to complete."), tr("Start Download"),
lambda: ui_state.params.put_bool("OsmDbUpdatesCheck", True))
def _select_region(self, region_type):
is_country = region_type == "Country"
btn = self._country_btn if is_country else self._state_btn
btn.action_item.set_enabled(False)
btn.action_item.set_text(tr("FETCHING..."))
threading.Thread(target=self._do_select_region, args=(region_type, btn)).start()
def _handle_region_selection(self, region_type, locations, key, res, ref):
if res != DialogResult.CONFIRM or not ref:
if region_type == "State" and res == DialogResult.CANCEL:
if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"):
ui_state.params.remove("OsmLocationName")
ui_state.params.remove("OsmLocationTitle")
ui_state.params.remove("OsmLocal")
self._update_labels()
return
if region_type == "Country":
ui_state.params.put_bool("OsmLocal", True)
ui_state.params.remove("OsmStateName")
ui_state.params.remove("OsmStateTitle")
ui_state.params.put(f"{key}Name", ref)
name = next((n.data['display_name'] for n in locations if n.ref == ref), ref)
ui_state.params.put(f"{key}Title", name)
if ref == "US" and region_type == "Country":
self._select_region("State")
else:
self._update_db()
def _do_select_region(self, region_type, btn):
base_url = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/"
url = base_url + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json")
try:
data = requests.get(url, timeout=10).json()
locations = sorted([TreeNode(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()], key=lambda n: n.data['display_name'])
except Exception:
locations = []
if region_type == "State":
locations.insert(0, TreeNode(ref="All", data={'display_name': tr("All states (~6.0 GB)")}))
btn.action_item.set_enabled(True)
btn.action_item.set_text(tr("SELECT"))
key = "OsmLocation" if region_type == "Country" else "OsmState"
current = ui_state.params.get(f"{key}Name") or ""
dialog = TreeOptionDialog(tr(f"Select {region_type}"), [TreeFolder(folder="", nodes=locations)], current_ref=current, search_prompt="Perform a search")
dialog.on_exit = lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref)
gui_app.set_modal_overlay(dialog, callback=lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref))
def _update_labels(self):
downloading = bool(self._mem_params.get("OSMDownloadLocations"))
self._country_btn.set_enabled(not downloading)
self._state_btn.set_enabled(not downloading)
self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US")
self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName")))
self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "")
self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "")
pending = ui_state.params.get_bool("OsmDbUpdatesCheck")
if downloading or pending:
if downloading:
device._reset_interactive_timeout()
self._update_map_size()
self._progress.set_visible(True)
progress = ui_state.params.get("OSMDownloadProgress")
total = progress.get('total_files', 0) if progress else 0
done = progress.get('downloaded_files', 0) if progress else 0
failed = total > 0 and not downloading and done < total
if total > 0:
progress_perc = max(0.0, min(100.0, (done / total) * 100.0))
else:
progress_perc = 0.0
if failed:
text = "0% - Downloading Maps"
btn_text = tr("Error: Invalid download. Retry.")
self._current_percent = 0.0
elif total > 0 and downloading:
self._current_percent = progress_perc
perc_int = int(progress_perc)
text = f"{perc_int}% - Downloading Maps"
btn_text = f"{done}/{total} ({perc_int}%)"
else:
self._current_percent = 0.0
text = "0% - Downloading Maps"
btn_text = tr("Downloading Maps...")
self._progress.action_item.update(self._current_percent, text, show_progress=total > 0 and downloading and not failed)
self._update_btn.action_item.set_enabled(not downloading) # TODO-SP: introduce CANCEL database download with mapd
self._update_btn.action_item.set_value(btn_text)
self._country_btn.action_item.set_enabled(not downloading)
self._state_btn.action_item.set_enabled(not downloading)
self._delete_maps_btn.action_item.set_enabled(not downloading)
else:
self._progress.set_visible(False)
self._update_btn.action_item.set_enabled(True)
self._country_btn.action_item.set_enabled(True)
self._state_btn.action_item.set_enabled(True)
self._delete_maps_btn.action_item.set_enabled(True)
ts = ui_state.params.get("OsmDownloadedDate")
dt: datetime.datetime | None = None
if ts:
try:
ts_f = float(ts)
if ts_f > 0:
dt = datetime.datetime.fromtimestamp(ts_f, tz=datetime.UTC)
except (ValueError, TypeError):
dt = None
formatted = time_ago(dt)
self._update_btn.action_item.set_value(tr("Last checked {}").format(formatted))
def show_event(self):
self._scroller.show_event()
def _update_state(self):
now = monotonic()
if now - self._last_map_size_update >= 1.0:
self._last_map_size_update = now
self._update_labels()
def _render(self, rect):
self._scroller.render(rect)
@@ -9,35 +9,34 @@ from enum import IntEnum
import pyray as rl
from openpilot.selfdrive.ui.layouts.settings import settings as OP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.lib.multilang import tr_noop
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.lib.multilang import tr_noop
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller_tici import Scroller
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore
OP.PanelType = IntEnum(
"PanelType",
[es.name for es in OP.PanelType] + [
"SUNNYLINK",
@@ -46,7 +45,6 @@ OP.PanelType = IntEnum( # type: ignore
"CRUISE",
"VISUALS",
"DISPLAY",
"OSM",
"NAVIGATION",
"TRIPS",
"VEHICLE",
@@ -120,7 +118,6 @@ class SettingsLayoutSP(OP.SettingsLayout):
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"),
OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"),
OP.PanelType.OSM: PanelInfo(tr_noop("OSM"), OSMLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
# OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
@@ -4,23 +4,23 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from cereal import custom
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
from openpilot.system.ui.widgets.list_view import dual_button_item
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
import pyray as rl
if gui_app.sunnypilot_ui():
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
from openpilot.system.version import sunnylink_consent_version
class SunnylinkHeader(Widget):
@@ -160,14 +160,14 @@ class SunnylinkLayout(Widget):
self._sunnylink_description = SunnylinkDescriptionItem()
self._sunnylink_description.set_visible(False)
self._sponsor_btn = button_item(
self._sponsor_btn = button_item_sp(
title=tr("Sponsor Status"),
button_text=tr("SPONSOR"),
description=tr(
"Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."),
callback=lambda: self._handle_pair_btn(False)
)
self._pair_btn = button_item(
self._pair_btn = button_item_sp(
title=tr("Pair GitHub Account"),
button_text=tr("Not Paired"),
description=tr(
@@ -209,8 +209,8 @@ class SunnylinkLayout(Widget):
return items
@staticmethod
def _get_sunnylink_dongle_id() -> str | None:
return str(ui_state.params.get("SunnylinkDongleId") or (lambda: tr("N/A")))
def _get_sunnylink_dongle_id() -> str:
return ui_state.params.get("SunnylinkDongleId") or tr("N/A")
def _handle_pair_btn(self, sponsor_pairing: bool = False):
sunnylink_dongle_id = self._get_sunnylink_dongle_id()
@@ -302,6 +302,22 @@ class SunnylinkLayout(Widget):
self._restore_btn.set_text(tr("Restore Settings"))
def _sunnylink_toggle_callback(self, state: bool):
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
sl_enabled: bool = ui_state.params.get_bool("SunnylinkEnabled")
if state and not sl_consent and not sl_enabled:
def on_consent_done():
enabled = ui_state.params.get_bool("SunnylinkEnabled")
self._update_description(enabled)
gui_app.set_modal_overlay(None)
sl_terms_dlg = SunnylinkConsentPage(done_callback=on_consent_done)
gui_app.set_modal_overlay(sl_terms_dlg)
else:
ui_state.params.put_bool("SunnylinkEnabled", state)
self._update_description(state)
def _update_description(self, state: bool):
if state:
description = tr(
"Welcome back!! We're excited to see you've enabled sunnylink again!")
@@ -0,0 +1,87 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
import time
from dataclasses import dataclass
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.multilang import tr_noop
PING_TIMEOUT_NS = 80_000_000_000 # 80 seconds in nanoseconds
METRIC_HEIGHT = 126
METRIC_MARGIN = 30
METRIC_START_Y = 300
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
# Color scheme
class Colors:
WHITE = rl.WHITE
WHITE_DIM = rl.Color(255, 255, 255, 85)
GRAY = rl.Color(84, 84, 84, 255)
# Status colors
GOOD = rl.WHITE
WARNING = rl.Color(218, 202, 37, 255)
DANGER = rl.Color(201, 34, 49, 255)
PROGRESS = rl.Color(0, 134, 233, 255)
DISABLED = rl.Color(128, 128, 128, 255)
# UI elements
METRIC_BORDER = rl.Color(255, 255, 255, 85)
BUTTON_NORMAL = rl.WHITE
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
@dataclass(slots=True)
class MetricData:
label: str
value: str
color: rl.Color
def update(self, label: str, value: str, color: rl.Color):
self.label = label
self.value = value
self.color = color
class SidebarSP:
def __init__(self):
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
def _update_sunnylink_status(self):
if not ui_state.params.get_bool("SunnylinkEnabled"):
self._sunnylink_status.update(tr_noop("SUNNYLINK"), tr_noop("DISABLED"), Colors.DISABLED)
return
last_ping = ui_state.params.get("LastSunnylinkPingTime") or 0
dongle_id = ui_state.params.get("SunnylinkDongleId")
is_online = last_ping and (time.monotonic_ns() - last_ping) < PING_TIMEOUT_NS
is_temp_fault = ui_state.params.get_bool("SunnylinkTempFault")
is_registering = not is_temp_fault and dongle_id in (None, "", UNREGISTERED_SUNNYLINK_DONGLE_ID)
# Determine status/color pair based on priority
if last_ping:
status, color = (tr_noop("ONLINE"), Colors.GOOD) if is_online else (tr_noop("ERROR"), Colors.DANGER)
elif is_temp_fault:
status, color = (tr_noop("FAULT"), Colors.WARNING)
elif is_registering:
status, color = (tr_noop("REGIST..."), Colors.PROGRESS)
else:
status, color = (tr_noop("OFFLINE"), Colors.DANGER)
self._sunnylink_status.update(tr_noop("SUNNYLINK"), status, color)
def _draw_metrics_w_sunnylink(self, rect: rl.Rectangle, _temp, _panda, _connect):
metrics = [_temp, _panda, _connect, self._sunnylink_status]
start_y = int(rect.y) + METRIC_START_Y
available_height = max(0, int(HOME_BTN.y) - METRIC_MARGIN - METRIC_HEIGHT - start_y)
spacing = available_height / max(1, len(metrics) - 1)
return metrics, start_y, spacing

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