Compare commits

..

90 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
YassineYousfi
adbf68f771 FrameReader: add hwaccel arg and clear frames_cache (#36974) 2026-01-02 11:29:45 -08:00
YassineYousfi
f62177a827 FrameReader: use hwaccel auto (#36973)
* FrameReader: use hwaccel auto

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

* high

* i didn't do this
2025-12-31 12:43:36 -08:00
Adeeb Shihadeh
a5348b8679 crcmod -> crcmod-plus (#36968) 2025-12-29 16:41:52 -08: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
Trey Moen
3f1f7ad89c feat(esim): remove bootstrap (#36954)
feat: remove bootstrap
2025-12-24 09:04:34 -08: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
230 changed files with 6308 additions and 6558 deletions

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
.gitmodules vendored
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

2
Jenkinsfile vendored
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}

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

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;
}

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

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

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),

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')

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]:

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]:

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) \

View File

@@ -1 +1 @@
#define DEFAULT_MODEL "Dark Souls 2 (Default)"
#define DEFAULT_MODEL "WMI (Default)"

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"}},
@@ -151,6 +153,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"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"}},
@@ -168,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"}},
@@ -193,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"}},
@@ -229,38 +241,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.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"}},
// Torque lateral control custom params
{"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}},
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},

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)

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);

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

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

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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

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])

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

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

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

View File

@@ -1 +1 @@
#define COMMA_VERSION "0.10.3"
#define COMMA_VERSION "0.10.4"

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"

2
panda

Submodule panda updated: 5f3c09c910...f9cdec7f7b

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

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`

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"

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:

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

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

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)

View File

@@ -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),

View File

@@ -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)

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)

View File

@@ -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

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!")

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:

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)

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}")

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# type: ignore
import random
from collections import defaultdict

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# type: ignore
import os
import argparse

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# type: ignore
from collections import defaultdict
import argparse

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:

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

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

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):

BIN
selfdrive/mapd Executable file

Binary file not shown.

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()

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

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 ''

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: {

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

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')})

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)

View File

@@ -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)

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)

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']

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

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:

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)

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:

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")

View File

@@ -169,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:
@@ -240,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)

View File

@@ -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)

View File

@@ -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(_)

View File

@@ -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:

View File

@@ -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")

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:

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")

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()

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()

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

View File

@@ -15,10 +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.hud_renderer import HudRendererSP as HudRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP
from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP
from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated
@@ -224,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")

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)

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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -17,7 +17,6 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayo
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.osm import OSMLayout
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
@@ -37,7 +36,7 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
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"),

View File

@@ -12,7 +12,7 @@ from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLa
ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore
OP.PanelType = IntEnum(
"PanelType",
[es.name for es in OP.PanelType] + [
"SUNNYLINK",

View File

@@ -18,11 +18,12 @@ class RainbowPath:
BASE_ALPHA = 0.8
ALPHA_FADE = 0.3 # Alpha reduction from bottom to top
def __init__(self, num_segments: int = None, speed: float = None, saturation: float = None, lightness: float = None):
self.num_segments = num_segments if num_segments is not None else self.DEFAULT_NUM_SEGMENTS
self.speed = speed if speed is not None else self.DEFAULT_SPEED
self.saturation = saturation if saturation is not None else self.DEFAULT_SATURATION
self.lightness = lightness if lightness is not None else self.DEFAULT_LIGHTNESS
def __init__(self, num_segments: int = DEFAULT_NUM_SEGMENTS, speed: float = DEFAULT_SPEED,
saturation: float = DEFAULT_SATURATION, lightness: float = DEFAULT_LIGHTNESS):
self.num_segments = num_segments
self.speed = speed
self.saturation = saturation
self.lightness = lightness
def set_speed(self, speed: float):
self.speed = speed

View File

@@ -4,26 +4,41 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import Enum
from cereal import messaging, log, custom
from openpilot.common.params import Params
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
from openpilot.system.ui.lib.application import gui_app
OpenpilotState = log.SelfdriveState.OpenpilotState
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
ONROAD_BRIGHTNESS_TIMER_PAUSED = -1
class OnroadTimerStatus(Enum):
NONE = 0
PAUSE = 1
RESUME = 2
class UIStateSP:
def __init__(self):
self.params = Params()
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
"gpsLocation", "liveTorqueParameters", "carStateSP", "carParamsSP", "liveDelay",
"mapdOut"
]
self.sunnylink_state = SunnylinkState()
self.update_params()
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.global_brightness_override: int = self.params.get("Brightness", return_default=True)
self.reset_onroad_sleep_timer()
def update(self) -> None:
if self.sunnylink_enabled:
@@ -31,6 +46,40 @@ class UIStateSP:
else:
self.sunnylink_state.stop()
def onroad_brightness_handle_alerts(self, started: bool, alert):
has_alert = started and self.onroad_brightness != OnroadBrightness.AUTO and alert is not None
self.update_onroad_brightness(has_alert)
if has_alert:
self.reset_onroad_sleep_timer()
def update_onroad_brightness(self, has_alert: bool) -> None:
if has_alert:
return
if self.onroad_brightness_timer > 0:
self.onroad_brightness_timer -= 1
def reset_onroad_sleep_timer(self, timer_status: OnroadTimerStatus = OnroadTimerStatus.NONE) -> None:
# Toggling from active state to inactive
if timer_status == OnroadTimerStatus.PAUSE and self.onroad_brightness_timer != ONROAD_BRIGHTNESS_TIMER_PAUSED:
self.onroad_brightness_timer = ONROAD_BRIGHTNESS_TIMER_PAUSED
# Toggling from a previously inactive state or resetting an active timer
elif (self.onroad_brightness_timer_param >= 0 and self.onroad_brightness != OnroadBrightness.AUTO and
self.onroad_brightness_timer != ONROAD_BRIGHTNESS_TIMER_PAUSED) or timer_status == OnroadTimerStatus.RESUME:
if self.onroad_brightness == OnroadBrightness.AUTO_DARK:
self.onroad_brightness_timer = 15 * gui_app.target_fps
else:
self.onroad_brightness_timer = self.onroad_brightness_timer_param * gui_app.target_fps
@property
def onroad_brightness_timer_expired(self) -> bool:
return self.onroad_brightness != OnroadBrightness.AUTO and self.onroad_brightness_timer == 0
@property
def auto_onroad_brightness(self) -> bool:
return self.onroad_brightness in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)
@staticmethod
def update_status(ss, ss_sp, onroad_evt) -> str:
state = ss.state
@@ -78,7 +127,10 @@ class UIStateSP:
self.chevron_metrics = self.params.get("ChevronInfo")
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
self.global_brightness_override = self.params.get("Brightness", return_default=True)
# Onroad Screen Brightness
self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True)))
self.onroad_brightness_timer_param = self.params.get("OnroadScreenOffTimer", return_default=True)
class DeviceSP:
@@ -88,3 +140,38 @@ class DeviceSP:
def _set_awake(self, on: bool):
if on and self._params.get("DeviceBootMode", return_default=True) == 1:
self._params.put_bool("OffroadMode", True)
@staticmethod
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
if not awake or not _ui_state.started:
return cur_brightness
if _ui_state.onroad_brightness_timer != 0:
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
return max(30.0, cur_brightness)
# For AUTO (Default) and Manual modes (while timer running), use standard brightness
return cur_brightness
# 0: Auto (Default), 1: Auto (Dark)
if _ui_state.onroad_brightness == OnroadBrightness.AUTO:
return cur_brightness
elif _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
return cur_brightness
# 2-21: 5% - 100%
return float((_ui_state.onroad_brightness - 1) * 5)
@staticmethod
def set_min_onroad_brightness(_ui_state, min_brightness: int) -> int:
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
min_brightness = 10
return min_brightness
@staticmethod
def wake_from_dimmed_onroad_brightness(_ui_state, evs) -> None:
if _ui_state.started and (_ui_state.onroad_brightness_timer_expired or _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK):
if any(ev.left_down for ev in evs):
if _ui_state.onroad_brightness_timer_expired:
gui_app.mouse_events.clear()
_ui_state.reset_onroad_sleep_timer()

View File

@@ -0,0 +1,3 @@
# Multilanguage
[![languages](https://raw.githubusercontent.com/commaai/openpilot/badges/translation_badge.svg)](#)

View File

@@ -200,7 +200,7 @@ msgstr "CONNECT"
#: system/ui/widgets/network.py:369
#, python-format
msgid "CONNECTING..."
msgstr "CONNECTING..."
msgstr "连接中..."
#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35
#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318
@@ -1001,7 +1001,7 @@ msgstr "用于“{}”"
#: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format
msgid "km/h"
msgstr "公里/时"
msgstr "km/h"
#: system/ui/widgets/network.py:204
#, python-format
@@ -1021,7 +1021,7 @@ msgstr "计量"
#: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format
msgid "mph"
msgstr "英里/时"
msgstr "mph"
#: selfdrive/ui/layouts/settings/software.py:20
#, python-format

View File

@@ -200,7 +200,7 @@ msgstr "CONNECT"
#: system/ui/widgets/network.py:369
#, python-format
msgid "CONNECTING..."
msgstr "CONNECTING..."
msgstr "連線中..."
#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35
#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318
@@ -1000,7 +1000,7 @@ msgstr "適用於「{}」"
#: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format
msgid "km/h"
msgstr "公里/時"
msgstr "km/h"
#: system/ui/widgets/network.py:204
#, python-format
@@ -1020,7 +1020,7 @@ msgstr "計量"
#: selfdrive/ui/onroad/hud_renderer.py:177
#, python-format
msgid "mph"
msgstr "英里/時"
msgstr "mph"
#: selfdrive/ui/layouts/settings/software.py:20
#, python-format

View File

@@ -18,7 +18,7 @@ OPENAI_PROMPT = "You are a professional translator from English to {language} (I
"The following sentence or word is in the GUI of a software called openpilot, translate it accordingly."
def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]:
def get_language_files(languages: list[str] | None = None) -> dict[str, pathlib.Path]:
files = {}
with open(TRANSLATIONS_LANGUAGES) as fp:

View File

@@ -258,17 +258,16 @@ class Device(DeviceSP):
else:
clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0
min_brightness = 30
if gui_app.sunnypilot_ui():
if ui_state.global_brightness_override <= 0:
min_global_brightness = 1 if ui_state.global_brightness_override < 0 else 30
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [min_global_brightness, 100]))
else:
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100]))
min_brightness = DeviceSP.set_min_onroad_brightness(ui_state, min_brightness)
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [min_brightness, 100]))
brightness = round(self._brightness_filter.update(clipped_brightness))
if gui_app.sunnypilot_ui() and ui_state.global_brightness_override > 0:
brightness = ui_state.global_brightness_override
if gui_app.sunnypilot_ui():
brightness = DeviceSP.set_onroad_brightness(ui_state, self._awake, brightness)
if not self._awake:
brightness = 0
@@ -285,6 +284,9 @@ class Device(DeviceSP):
self._ignition = ui_state.ignition
if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events):
if gui_app.sunnypilot_ui():
DeviceSP.wake_from_dimmed_onroad_brightness(ui_state, gui_app.mouse_events)
self._reset_interactive_timeout()
interaction_timeout = time.monotonic() > self._interaction_time

View File

@@ -1,3 +1,4 @@
SConscript(['common/transformations/SConscript'])
SConscript(['modeld/SConscript'])
SConscript(['modeld_v2/SConscript'])
SConscript(['selfdrive/locationd/SConscript'])
SConscript(['selfdrive/locationd/SConscript'])

View File

@@ -1,129 +0,0 @@
<center>
# Comparative Analysis of Parameter Getter Methods: Params vs. ParamWatcher
James (sunnypilot Developer) <br>
December 13, 2025
</center>
## <br><br> Abstract
This research report examines the inefficiencies in standard parameter access methods within sunnypilot and proposes an optimized alternative, ParamWatcher. The standard `Params::get()` method incurs significant CPU and memory overhead due to repeated file I/O operations (sunnypilot, 2025). ParamWatcher utilizes OS-level file system events (inotify on Linux, FSEvents on macOS) to maintain an in-memory cache, reducing I/O to near zero (Linux man-pages, 2025-c; Apple Inc., n.d.-a). Empirical benchmarks with ~10 million parameter accesses demonstrate a 14.5x CPU speedup and flat memory usage (514.7 KB vs. 497.7 KB for base Params, with only 17 KB overhead). The implementation employs a process-local singleton pattern for efficiency in multi-process architectures (Gamma et al., 1994). Results indicate ParamWatcher eliminates UI stutters and GC pauses, enhancing system responsiveness without compromising data freshness.
**Keywords:** parameter access, file I/O optimization, event-driven caching, autonomous driving systems, performance benchmarking
## Introduction
In sunnypilot, efficient parameter management is important for real-time system access. The standard `Params::get()` method, implemented in C++ and wrapped in Python, performs full file I/O cycles for each access, leading to high CPU overhead and memory churn (sunnypilot, 2025). This is particularly problematic in UI loops where parameters are queried frequently (e.g., 50 toggles at 20 FPS equates to ~1,000 reads/second).
This inefficiency stems from architectural mismatches: C++ streams are designed for throughput, not latency (cppreference.com, n.d.-a). Each call triggers kernel mode switches, heap allocations, and garbage collection in Python, causing UI stutters (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
### Inefficiencies in Standard Parameter Access
The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop.
#### System Overhead Analysis
- **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025).
- **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
- **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025).
- **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.-b).
This report introduces ParamWatcher, an event-driven caching solution using OS file system events. It shifts from polling to notifications, caching converted values in static RAM. I propose that ParamWatcher achieves minimum 10x+ CPU gains with bounded non increasing memory, improving sunnypilot's performance both on latency and responsiveness.
## Method
### Materials
- **System:** sunnypilot, running on macOS, Ubuntu/Linux, comma 3x, and comma four.
- **Parameters:** 231 defined keys in `param_keys.h`.
- **Tools:** Python 3, tracemalloc for memory profiling, time.perf_counter for CPU timing, ctypes for OS integration (Python Software Foundation, 2025).
### Implementation Details
ParamWatcher provides cross-platform file system monitoring using ctypes for direct OS integration (Python Software Foundation, 2025).
#### Linux Implementation
On Linux, ParamWatcher uses the inotify subsystem for efficient file change detection (Linux man-pages, 2025-c). It loads `libc.so.6` to access system calls, initializes an inotify instance, and watches the parameters directory for events like `IN_MODIFY` and `IN_CLOSE_WRITE` (Linux Kernel Organization, 2005). Events are polled with `select.epoll()` and parsed using `struct.unpack_from()` to avoid ctypes overhead. Filenames are extracted and passed to cache invalidation, ensuring real-time updates without polling (Codezup, 2025).
- **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls.
- **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor.
- **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes.
- **Event Loop**:
- **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting.
- **Reading**: When events occur, `os.read(fd, 2048)` retrieves the raw binary event data.
- **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures.
- **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access.
#### macOS Implementation
On macOS, ParamWatcher leverages FSEvents from CoreServices for directory monitoring (Apple Inc., n.d.-a). It defines a C-compatible callback using `CFUNCTYPE`, creates an `FSEventStream` with `kFSEventStreamCreateFlagFileEvents`, and schedules it on the run loop (Apple Inc., n.d.-b). Events are filtered for modifications, creations, and renames (Apple Inc., n.d.-c), triggering cache invalidation for affected parameters.
- **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`.
- **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory.
- **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available.
- **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c).
- **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b).
- **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary.
- **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter.
### Procedure
Benchmarks simulated heavy load:
- **Memory Test:** ~10 million gets (43,290 loops over 231 keys), measured with tracemalloc.
- **CPU Test:** Same load, timed with perf_counter.
- Comparisons: Base Params vs. ParamWatcher.
## Results
### Memory Usage
Using tracemalloc for peak memory measurement during ~10 million parameter accesses (43,290 loops over 231 keys), base Params peaked at 497.7 KB, while ParamWatcher peaked at 514.7 KB (17 KB overhead). ParamWatcher's memory remained flat post-initialization, preventing churn.
| Condition | Memory (KB) | Overhead |
|-----------|-------------|----------|
| Base Params | 497.7 | - |
| ParamWatcher | 514.7 | 17 KB |
### CPU Performance
ParamWatcher was 14.5x faster: 4.52s vs. 65.43s to complete ~10 million param gets.
| Condition | Time (s) | Speedup |
|-----------|----------|---------|
| Base Params | 65.43 | 1x |
| ParamWatcher | 4.52 | 14.5x |
### Scalability
No degradation at scale; cache invalidation maintained freshness.
## Discussion
ParamWatcher successfully optimizes parameter access, delivering substantial CPU gains with minimal memory overhead. The event-driven approach eliminates I/O bottlenecks, reducing GC pressure and UI stutters (cppreference.com, n.d.-b). The 17 KB memory overhead is negligible compared to the megabytes of churn from base Params, ensuring bounded usage in multi-process environments via the singleton pattern (Gamma et al., 1994).
Results demonstrate scalability without degradation, with cache invalidation maintaining data freshness. This optimization enhances system responsiveness.
Limitations include potential event latency in high-load scenarios (<10 ms, imperceptible for UI) and increased complexity from background threads.
Trade-offs: Static RAM (~17 KB) vs. dynamic churn; benefits outweigh costs for param-heavy workloads.
## <br>References
Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events
Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop
Apple Inc. (n.d.-c). *FSEventStreamEventFlags*. Retrieved from https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags
Codezup. (2025). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/
cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cppreference.com/w/cpp/io/basic_ifstream
cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
Linux Kernel Organization. (2005). *include/uapi/linux/inotify.h*. Retrieved from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/inotify.h
Linux man-pages. (2025-a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html
Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html
Linux man-pages. (2025-c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html
Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html
sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2
sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc#L79C1-L117C2

View File

@@ -1,193 +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 os
import platform
import struct
import select
import threading
import time
import ctypes
import ctypes.util
import traceback
from ctypes import c_void_p, c_size_t, POINTER, c_uint32, c_uint64
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.hw import Paths
IN_MODIFY = 0x00000002
IN_CREATE = 0x00000100
IN_DELETE = 0x00000200
IN_MOVED_TO = 0x00000080
IN_CLOSE_WRITE = 0x00000008
class ParamWatcher(Params):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
super().__init__()
cloudlog.warning("ParamWatcher initialized")
self._cache = {}
self._last_trigger = {}
self._version = {}
self._lock = threading.Lock()
self._callbacks = []
self.last_accessed_param = None
self._initialized = True
self.start()
def start(self):
if getattr(self, '_thread', None) and self._thread.is_alive():
return
self._thread = threading.Thread(target=self._run_watcher, daemon=True)
self._thread.start()
def is_watching(self):
return getattr(self, '_thread', None) and self._thread.is_alive()
def add_watcher(self, callback):
if callback not in self._callbacks:
self._callbacks.append(callback)
def _trigger_callbacks(self, path):
with self._lock:
if (now := time.monotonic()) - self._last_trigger.get(path, 0) < 0.1:
return
self._last_trigger[path] = now
self._version[path] = self._version.get(path, 0) + 1
self._cache.pop(path, None)
for callback in self._callbacks:
try:
callback(path)
except Exception:
cloudlog.exception("Param watcher callback failed")
def _get_cached(self, key, getter, sig):
k = str(key)
with self._lock:
bucket = self._cache.get(k)
if bucket and sig in bucket:
if bucket[sig][0] == self._version.get(k, 0):
return bucket[sig][1]
start_ver = self._version.get(k, 0)
val = getter()
with self._lock:
if self._version.get(k, 0) != start_ver:
val = getter()
self._cache.setdefault(k, {})[sig] = (self._version.get(k, 0), val)
return val
def get(self, key, block=False, return_default=False):
self.last_accessed_param = key
if block:
return super().get(key, block, return_default)
fetcher = super().get
return self._get_cached(key, lambda: fetcher(key, block, return_default), (block, return_default))
def get_bool(self, key, block=False):
self.last_accessed_param = key
if block:
return super().get_bool(key, block)
fetcher = super().get_bool
return self._get_cached(key, lambda: fetcher(key, block), ("bool", block))
def _run_watcher(self):
system = platform.system()
while True:
try:
if system == "Linux":
self._run_linux()
elif system == "Darwin":
self._run_darwin()
except Exception:
cloudlog.exception("Param watcher crashed")
time.sleep(2)
def _run_linux(self):
path = Paths.params_root()
libc = ctypes.CDLL('libc.so.6')
fd = libc.inotify_init()
libc.inotify_add_watch(fd, path.encode(), IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE)
try:
poll = select.epoll()
poll.register(fd, select.EPOLLIN)
while True:
for fileno, _ in poll.poll():
if fileno == fd:
buffer = os.read(fd, 2048)
i = 0
while i + 16 <= len(buffer):
_, mask, _, name_len = struct.unpack_from("iIII", buffer, i)
if mask & (IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE):
name = buffer[i+16:i+16+name_len].rstrip(b"\0").decode()
if not name.startswith("."):
self._trigger_callbacks(name)
i += 16 + name_len
finally:
if 'poll' in locals():
poll.unregister(fd)
poll.close()
os.close(fd)
def _run_darwin(self):
CS = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreServices"))
CF = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
kCFAllocatorDefault = c_void_p(0)
kCFStringEncodingUTF8 = 0x08000100
kFSEventStreamCreateFlagFileEvents = 0x00000010
kFSEventStreamEventFlagItemCreated = 0x00000100
kFSEventStreamEventFlagItemRemoved = 0x00000200
kFSEventStreamEventFlagItemRenamed = 0x00000800
kFSEventStreamEventFlagItemModified = 0x00001000
CF.CFStringCreateWithCString.restype = c_void_p
CF.CFStringCreateWithCString.argtypes = [c_void_p, ctypes.c_char_p, c_uint32]
CF.CFArrayCreate.restype = c_void_p
CF.CFArrayCreate.argtypes = [c_void_p, POINTER(c_void_p), c_size_t, c_void_p]
CS.FSEventStreamCreate.restype = c_void_p
CS.FSEventStreamCreate.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, ctypes.c_double, c_uint32]
CS.FSEventStreamScheduleWithRunLoop.argtypes = [c_void_p, c_void_p, c_void_p]
CS.FSEventStreamStart.argtypes = [c_void_p]
CF.CFRunLoopGetCurrent.restype = c_void_p
def _cb(stream, ctx, num, paths, flags, ids):
try:
paths_arr = ctypes.cast(paths, POINTER(c_void_p))
flags_arr = ctypes.cast(flags, POINTER(c_uint32))
for i in range(num):
path = ctypes.cast(paths_arr[i], ctypes.c_char_p).value
if path and (flags_arr[i] & (kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRemoved |
kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemModified)):
self._trigger_callbacks(os.path.basename(path.decode('utf-8').rstrip('/')))
except Exception:
traceback.print_exc()
self._darwin_cb = ctypes.CFUNCTYPE(None, c_void_p, c_void_p, c_size_t, c_void_p, POINTER(c_uint32), POINTER(c_uint64))(_cb)
path_str = Paths.params_root().encode('utf-8')
cf_path = CF.CFStringCreateWithCString(kCFAllocatorDefault, path_str, kCFStringEncodingUTF8)
cf_paths = CF.CFArrayCreate(kCFAllocatorDefault, (c_void_p * 1)(cf_path), 1, None)
stream = CS.FSEventStreamCreate(kCFAllocatorDefault, self._darwin_cb, None, cf_paths, -1, 0.05, kFSEventStreamCreateFlagFileEvents)
run_loop = CF.CFRunLoopGetCurrent()
kDefaultMode = CF.CFStringCreateWithCString(kCFAllocatorDefault, b"kCFRunLoopDefaultMode", kCFStringEncodingUTF8)
CS.FSEventStreamScheduleWithRunLoop(stream, run_loop, kDefaultMode)
CS.FSEventStreamStart(stream)
CF.CFRunLoopRun()

View File

@@ -1,4 +0,0 @@
from openpilot.common.params_pyx import ParamKeyFlag, ParamKeyType, UnknownKeyName
from openpilot.sunnypilot.common.param_watcher import ParamWatcher as Params
__all__ = ["Params", "ParamKeyFlag", "ParamKeyType", "UnknownKeyName"]

View File

@@ -1,94 +0,0 @@
import time
import pytest
import threading
import tracemalloc
from openpilot.common.params import Params
from openpilot.common.params_pyx import UnknownKeyName
from openpilot.sunnypilot.common.param_watcher import ParamWatcher
class TestParamWatcher:
BYTES_KEYS = ["LocationFilterInitialState", "UpdaterCurrentReleaseNotes", "UpdaterNewReleaseNotes"]
BOOL_KEYS = [
"IsMetric", "AdbEnabled", "AlwaysOnDM", "ExperimentalMode",
"ExperimentalModeConfirmed", "DisengageOnAccelerator",
"OpenpilotEnabledToggle", "RecordAudio", "RecordFront"
]
_key_counter = 0
@pytest.fixture(autouse=True)
def setup_method(self):
self.params = Params()
self.key_index = TestParamWatcher._key_counter
TestParamWatcher._key_counter += 1
self.bytes_key = self.BYTES_KEYS[self.key_index % len(self.BYTES_KEYS)]
self.bool_key = self.BOOL_KEYS[self.key_index % len(self.BOOL_KEYS)]
@pytest.fixture
def param_watcher(self):
ParamWatcher._instance = None
param_watch = ParamWatcher()
param_watch.start()
assert param_watch.is_watching(), "ParamWatcher thread died"
return param_watch
def teardown_method(self):
for key in (self.bytes_key, self.bool_key):
try:
self.params.remove(key)
except UnknownKeyName:
pass
def test_watcher_detects_change(self, param_watcher):
val = b"123"
self.params.put(self.bytes_key, val)
assert param_watcher.get(self.bytes_key) == val
def test_watcher_get_bool(self, param_watcher):
self.params.put_bool(self.bool_key, True)
assert param_watcher.get_bool(self.bool_key) is True # First read should populate internal cache
def test_performance_comparison(self, param_watcher):
plain_params = self.params
for key in self.BYTES_KEYS:
plain_params.put(key, b"x" * 10000)
param_watcher.get(key)
for key in self.BOOL_KEYS:
plain_params.put_bool(key, True)
param_watcher.get_bool(key)
def bench(get_bytes, get_bool):
tracemalloc.start()
start_time = time.process_time()
for _ in range(1000):
for key in self.BYTES_KEYS:
get_bytes(key)
for key in self.BOOL_KEYS:
get_bool(key)
duration = time.process_time() - start_time
_, memory = tracemalloc.get_traced_memory()
tracemalloc.stop()
return duration, memory
plain_cpu, plain_memory = bench(plain_params.get, plain_params.get_bool)
watcher_cpu, watcher_memory = bench(param_watcher.get, param_watcher.get_bool)
# ParamWatcher *should* be significantly faster and use less memory than Params()
assert watcher_cpu < plain_cpu * 0.6, f"PW CPU ({watcher_cpu:.4f}s) should be < 60% of Param call ({plain_cpu:.4f}s)"
assert watcher_memory < plain_memory * 0.5, f"PW Memory ({watcher_memory}B) should be < 50% of Param call ({plain_memory}B)"
def test_cache_invalidation_simulation(self, param_watcher):
self.params.put(self.bytes_key, b"old")
assert param_watcher.get(self.bytes_key) == b"old"
time.sleep(0.2)
event = threading.Event()
param_watcher.add_watcher(lambda key: event.set())
param_watcher._trigger_callbacks(self.bytes_key)
assert event.wait(timeout=2), "Callback not triggered"
self.params.put(self.bytes_key, b"new")
assert param_watcher.get(self.bytes_key) == b"new"

View File

@@ -0,0 +1,4 @@
Import('env')
transformations = env.Library('transformations', ['orientation.cc', 'coordinates.cc'])
Export('transformations')

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