Compare commits

...

423 Commits

Author SHA1 Message Date
nayan
9f17e34e1f Merge remote-tracking branch 'origin/sync-20251114' into nayan-raylib 2025-11-15 08:52:45 -05:00
Jason Wen
73dca101a2 Revert "let's update docs here"
This reverts commit 51fe03cd51.
2025-11-15 02:10:32 -05:00
Jason Wen
51fe03cd51 let's update docs here 2025-11-15 02:06:45 -05:00
Jason Wen
201504ab27 too extra 2025-11-15 01:53:04 -05:00
Jason Wen
88d03af58a need to bring this back 2025-11-15 01:51:50 -05:00
Jason Wen
0f6ea18d14 too extra red diff on the side 2025-11-15 01:39:21 -05:00
nayan
a453aa2c51 tree option dialog + sample 2025-11-14 20:34:18 -05:00
nayan
d08f685982 fixes for latest merge 2025-11-14 20:34:03 -05:00
nayan
cb4d13643a Merge remote-tracking branch 'origin/sync-20251114' into nayan-raylib
# Conflicts:
#	selfdrive/ui/layouts/settings/developer.py
#	selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
#	selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h
#	system/manager/process_config.py
#	system/ui/lib/scroll_panel.py
#	system/ui/sunnypilot/lib/application.py
2025-11-14 19:55:13 -05:00
Jason Wen
9e0a88bc5e no more 2025-11-14 03:47:34 -05:00
Jason Wen
77a1c7a7a6 more fixes 2025-11-14 03:25:45 -05:00
Jason Wen
32ca63afd1 quiet mode back 2025-11-14 03:24:48 -05:00
Jason Wen
abceb1f936 raylib says wut 2025-11-14 03:21:18 -05:00
Jason Wen
63a3a95d65 should've been symlink'ed 2025-11-14 03:14:20 -05:00
Jason Wen
a77c5a97dc not yet 2025-11-14 02:57:56 -05:00
Jason Wen
370a7a8fa6 bump opendbc 2025-11-14 02:52:26 -05:00
Jason Wen
81ec281eca sum more 2025-11-14 02:51:54 -05:00
Jason Wen
ec48b66503 commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e 2025-11-14 02:25:10 -05:00
Jason Wen
a61839db40 commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e 2025-11-14 02:16:45 -05:00
Jason Wen
e55e0949e7 commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9 2025-11-14 02:12:47 -05:00
Jason Wen
a8ec2a4dc0 cabana: revert to stock Qt 2025-11-14 01:56:19 -05:00
Jason Wen
0c98c33e79 sunnypilot: remove Qt 2025-11-14 01:53:22 -05:00
Jason Wen
a6e43a77d9 bump opendbc 2025-11-14 01:49:18 -05:00
Jason Wen
9d1716c4f7 bump opendbc 2025-11-14 01:46:24 -05:00
Jason Wen
d9930a4f7c bump opendbc 2025-11-14 01:36:23 -05:00
Jason Wen
6f83806ae7 bump opendbc 2025-11-14 01:32:38 -05:00
Jason Wen
716e86c707 bump opendbc 2025-11-14 01:31:57 -05:00
Jason Wen
0d4cfa806b commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b 2025-11-14 01:19:17 -05:00
Jason Wen
c23516a8de Merge branch 'upstream/openpilot/master' into sync-20251114
# Conflicts:
#	.github/workflows/ci_weekly_run.yaml
#	.github/workflows/raylib_ui_preview.yaml
#	.github/workflows/tests.yaml
#	.gitmodules
#	README.md
#	SConstruct
#	common/api.py
#	common/params_keys.h
#	docs/CARS.md
#	msgq_repo
#	opendbc_repo
#	panda
#	selfdrive/car/tests/test_car_interfaces.py
#	selfdrive/controls/controlsd.py
#	selfdrive/controls/lib/latcontrol.py
#	selfdrive/controls/lib/latcontrol_angle.py
#	selfdrive/controls/lib/latcontrol_pid.py
#	selfdrive/controls/lib/latcontrol_torque.py
#	selfdrive/controls/tests/test_latcontrol.py
#	selfdrive/monitoring/helpers.py
#	selfdrive/ui/SConscript
#	selfdrive/ui/main.cc
#	selfdrive/ui/qt/body.h
#	selfdrive/ui/qt/home.cc
#	selfdrive/ui/qt/home.h
#	selfdrive/ui/qt/network/networking.cc
#	selfdrive/ui/qt/network/networking.h
#	selfdrive/ui/qt/network/wifi_manager.cc
#	selfdrive/ui/qt/offroad/developer_panel.cc
#	selfdrive/ui/qt/offroad/developer_panel.h
#	selfdrive/ui/qt/offroad/experimental_mode.cc
#	selfdrive/ui/qt/offroad/firehose.cc
#	selfdrive/ui/qt/offroad/firehose.h
#	selfdrive/ui/qt/offroad/onboarding.cc
#	selfdrive/ui/qt/offroad/onboarding.h
#	selfdrive/ui/qt/offroad/settings.cc
#	selfdrive/ui/qt/offroad/settings.h
#	selfdrive/ui/qt/offroad/software_settings.cc
#	selfdrive/ui/qt/onroad/alerts.cc
#	selfdrive/ui/qt/onroad/annotated_camera.h
#	selfdrive/ui/qt/onroad/buttons.cc
#	selfdrive/ui/qt/onroad/buttons.h
#	selfdrive/ui/qt/onroad/driver_monitoring.cc
#	selfdrive/ui/qt/onroad/hud.cc
#	selfdrive/ui/qt/onroad/hud.h
#	selfdrive/ui/qt/onroad/model.cc
#	selfdrive/ui/qt/onroad/model.h
#	selfdrive/ui/qt/onroad/onroad_home.cc
#	selfdrive/ui/qt/onroad/onroad_home.h
#	selfdrive/ui/qt/request_repeater.h
#	selfdrive/ui/qt/sidebar.cc
#	selfdrive/ui/qt/sidebar.h
#	selfdrive/ui/qt/util.cc
#	selfdrive/ui/qt/widgets/cameraview.h
#	selfdrive/ui/qt/widgets/controls.cc
#	selfdrive/ui/qt/widgets/controls.h
#	selfdrive/ui/qt/widgets/input.cc
#	selfdrive/ui/qt/widgets/input.h
#	selfdrive/ui/qt/widgets/prime.cc
#	selfdrive/ui/qt/widgets/prime.h
#	selfdrive/ui/qt/widgets/ssh_keys.h
#	selfdrive/ui/qt/widgets/toggle.h
#	selfdrive/ui/qt/widgets/wifi.cc
#	selfdrive/ui/qt/widgets/wifi.h
#	selfdrive/ui/qt/window.cc
#	selfdrive/ui/qt/window.h
#	selfdrive/ui/tests/cycle_offroad_alerts.py
#	selfdrive/ui/tests/test_ui/run.py
#	selfdrive/ui/translations/main_ar.ts
#	selfdrive/ui/translations/main_de.ts
#	selfdrive/ui/translations/main_es.ts
#	selfdrive/ui/translations/main_fr.ts
#	selfdrive/ui/translations/main_ja.ts
#	selfdrive/ui/translations/main_ko.ts
#	selfdrive/ui/translations/main_nl.ts
#	selfdrive/ui/translations/main_pl.ts
#	selfdrive/ui/translations/main_pt-BR.ts
#	selfdrive/ui/translations/main_th.ts
#	selfdrive/ui/translations/main_tr.ts
#	selfdrive/ui/translations/main_zh-CHS.ts
#	selfdrive/ui/translations/main_zh-CHT.ts
#	selfdrive/ui/ui.cc
#	selfdrive/ui/ui.h
#	system/manager/build.py
#	system/version.py
2025-11-14 01:11:37 -05:00
nayan
1a4992d390 Merge remote-tracking branch 'origin/master' into nayan-raylib
# Conflicts:
#	common/params_keys.h
#	selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
#	selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h
2025-11-13 18:15:13 -05:00
Maxime Desroches
9c19ec8409 bump raylib 2025-11-13 15:09:33 -08:00
Shane Smiskol
fc253fe1ee Don't resize images that are the same size 2025-11-13 14:59:34 -08:00
Shane Smiskol
d72a01d739 raylib: fix texture wrapping filtering artifacts (#36618)
fix wrapping artifacts
2025-11-13 14:58:16 -08:00
Trey Moen
f93b3f51c9 fix: install missing x deps for building raylib from src (#36614)
* fix: install missing x deps for building raylib from src

* move here

* cleaner
2025-11-12 20:04:20 -08:00
Dean Lee
3d08a5048b replay: Only send bookmarkButton message when --all flag is set (#36612)
Only send BookmarkButton message when --all flag is set
2025-11-12 14:22:14 -08:00
Maxime Desroches
9ee66008db AGNOS 15 (#36611)
* stage

* production
2025-11-11 22:59:54 -08:00
Dean Lee
6a257fe2de ui: increase profile output from 25 to 100 functions (#36607)
increase profile output from 20 to 100 functions
2025-11-11 16:10:37 -08:00
Maxime Desroches
dad7bb53a2 ui: let ui_state set brightness 2025-11-10 22:24:35 -08:00
Maxime Desroches
47ba86af33 enable ADB in release 2025-11-10 19:57:59 -08:00
Trey Moen
9689de426b chore: adb rules (#36544)
* chore: adb rules

* i think 51 is common, lets use our own
2025-11-10 18:38:49 -08:00
Dean Lee
124eb42758 ui: fix CameraView crash caused by stale frame (#36563)
fix CameraView crash from stale frame
2025-11-10 18:10:50 -08:00
Trey Moen
85404c184b fix: badges (#36566)
* re-add

* need to validate

* ok looks good

* oops

* lint
2025-11-10 18:08:55 -08:00
Dean Lee
ed42cfe699 ui: refactor GuiApplication.render into smaller helper methods (#36569)
refactor render into smaller helper method
2025-11-10 18:08:02 -08:00
Dean Lee
3099f4f12d ui: cache the version text to avoid redundant Params.get calls every frame (#36601)
cache the version text to avoid redundant Params.get calls every frame
2025-11-10 18:05:55 -08:00
Dean Lee
8fceb9d957 cabana: replace deprecated Qt and OpenSSL functions (#36605)
replace deprecated functions
2025-11-10 13:58:23 -08:00
Jason Young
d4185a5d57 docs: car porting (#36590)
* checkpoint

* door states, notes

* updates

* not worth it yet

* wordsmith

* more

* more reverse engineering script content

* Revise stationary ignition-only test steps

Updated the steps for stationary ignition-only tests to include closing the driver's door and fastening the seatbelt before pressing the accelerator and brake pedals.

* fix numbering
2025-11-07 20:37:40 -05:00
ZwX1616
1262fca36b add check driver camera alert (#36577)
* add event

* missing arg

* creation_delay is wrong

* add logging

* set offroad alert

* Update selfdrive/selfdrived/alerts_offroad.json

Co-authored-by: Shane Smiskol <shane@smiskol.com>

* rm onard

* add details

* rename to DM

* log rename

* no poss

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-11-07 15:18:45 -08:00
Maxime Desroches
890b1cf512 AGNOS 14.7 (#36597)
* stage

* prod
2025-11-07 02:54:52 -08:00
Maxime Desroches
1633641055 bump raylib (#36596)
this
2025-11-06 21:00:26 -08:00
David
2dcb67091f remove unused MAX_POINTS constant from model_renderer.py (#36593) 2025-11-06 20:51:17 -08:00
Maxime Desroches
fb34601d5a Revert "bump panda" (#36594)
This reverts commit 36e53c7394.
2025-11-06 20:46:04 -08:00
Adeeb Shihadeh
b6bcc8cca3 ui: fix running on macOS 2025-11-06 09:42:23 -08:00
Maxime Desroches
e5a7deb6ad AGNOS 14.6 (#36586)
* stage

* ver

* prod
2025-11-06 01:22:37 -08:00
Maxime Desroches
10100e34e1 bump raylib 2025-11-06 01:04:17 -08:00
Adeeb Shihadeh
2d31b422c8 ui: prep for 60fps (#36585) 2025-11-05 23:01:10 -08:00
Adeeb Shihadeh
89919c8832 this is not a good api 2025-11-05 18:55:50 -08:00
Adeeb Shihadeh
dc5f5eaf65 make github LFS work if you want it 2025-11-05 16:34:19 -08:00
Adeeb Shihadeh
ee8970dc42 ui: add route-based profiler (#36576)
* ui: add route-based profiler

* cleanup

* this is stupid
2025-11-05 16:23:33 -08:00
David
0a44b48e21 gitignore: add raylib test UI screenshots report path (#36570)
ui: update .gitignore to include raylib_report
2025-11-05 10:35:56 -08:00
Maxime Desroches
36e53c7394 bump panda 2025-11-04 19:18:16 -08:00
ZwX1616
38eb400e41 monitoring: account for OS cam distribution shift (#36567)
* this should match

* roughly matching FPR at 2 to 1 cost
2025-11-04 16:00:15 -08:00
Maxime Desroches
5198b1b079 support ECDSA (#36555)
* keys

* remove

* remove

* too small
2025-11-03 22:45:00 -08:00
Adeeb Shihadeh
e8a11591a8 ui: add render loop profiling (#36558) 2025-11-03 21:45:46 -08:00
Adeeb Shihadeh
cbc8f98682 ui: fix RuntimeError on exit on PC 2025-11-03 16:20:36 -08:00
Adeeb Shihadeh
ecdcb5d0c6 tici: affine DRM IRQ to same core as ui (#36554) 2025-11-03 14:36:19 -08:00
Adeeb Shihadeh
c7494aed0f ui: move to GPU core (#36553)
* ui: move to GPU core

* we're on the big boy core now
2025-11-03 14:31:45 -08:00
Dean Lee
215ef16803 ui: fix LineSeparator horizontal centering issue (#36533)
fix LineSeparator horizontal centering issue
2025-11-03 11:22:42 -08:00
Dean Lee
350b846d3a ui: fix vertical centering for multi-line labels (#36538)
fix vertical centering for multi-line labels
2025-11-03 11:21:41 -08:00
Dean Lee
9ce9920ff7 ui: fix label text eliding to account for icon width (#36539)
fix label text eliding to account for icon width
2025-11-03 11:21:20 -08:00
Dean Lee
1c0b087105 ui: fix keyboard.reset() to properly clear all interaction state (#36541)
fix keyboard.reset() to properly clear all interaction state
2025-11-03 11:20:24 -08:00
Dean Lee
137d4b89b4 ui: fix icon vertical positioning using width instead of height (#36542)
fix icon vertical positioning using width instead of height
2025-11-03 11:20:08 -08:00
Shane Smiskol
2cc4885a2e raylib: fix window freezing (#36517)
fix window freezing
2025-11-03 11:17:38 -08:00
felsager
736e1fa7b7 Revert "latcontrol_torque: make feed-forward jerk independent of individual platform lag (#36334)"
This reverts commit fc4e5007fd.
2025-11-03 10:31:27 -08:00
felsager
177c7f1cf3 Revert "latcontrol_torque: retune torque controller (#36392)"
This reverts commit 76c5cb6d87.
2025-11-03 10:31:22 -08:00
Dean Lee
9bf904e8a6 ui: scale mouse positions in touch history (#36530)
scale mouse position in touch history
2025-11-03 08:55:05 -08:00
Adeeb Shihadeh
5ea5f6f267 ui: timeout touch points (#36550) 2025-11-02 21:48:33 -08:00
Shane Smiskol
525b6e48e9 raylib: fix word wrap (#36545)
* fix word wrap underestimating width

* and that
2025-11-02 04:32:29 -08:00
Shane Smiskol
c7b115b68e raylib: fix text measure with emojis (#36546)
fix
2025-11-02 04:30:08 -08:00
Dean Lee
62aef9cd34 tools: update README (#36531)
update readme
2025-10-31 08:16:55 -07:00
Adeeb Shihadeh
f57617c944 expose more state from gui_app 2025-10-30 16:03:28 -07:00
Adeeb Shihadeh
c4a0e57046 ui: add debug toggle (#36529)
* ui: add debug toggle

* initial state
2025-10-30 15:52:56 -07:00
felsager
76c5cb6d87 latcontrol_torque: retune torque controller (#36392) 2025-10-30 13:34:44 -07:00
felsager
fc4e5007fd latcontrol_torque: make feed-forward jerk independent of individual platform lag (#36334) 2025-10-30 13:29:38 -07:00
Dean Lee
af24fd6842 remove qrcode library from third_party (#36528) 2025-10-30 09:24:22 -07:00
Maxime Desroches
002a22a097 AGNOS 14.5 (#36523)
* stage

* updater

* prod
2025-10-28 23:05:44 -07:00
Maxime Desroches
9f20eb8ce6 setup: handle incompatible versions (#36520)
check
2025-10-28 19:15:43 -07:00
Adeeb Shihadeh
2e636458a6 op adb: forward all openpilot service ports (#36518)
* op adb: forward all openpilot service ports

* cleanup
2025-10-28 16:47:22 -07:00
David
47d0a95fd6 font: remove unifont anti-aliasing and reduce font size to 16 (#36508)
remove unifont anti-aliasing and reduce font size to 16

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-10-28 14:49:33 -07:00
Dean Lee
5d142326f5 ui: remove unused get_width() method (#36512)
remove unused get_width() method
2025-10-28 14:47:17 -07:00
Dean Lee
ef9683ee79 ui: skip rendering when screen is off (#36510)
* skip rendering when screen is off

* continue and rename

* revert that

* flip

---------

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-10-28 13:09:49 -07:00
Maxime Desroches
8a77534d02 fix zipapp with multilang (#36511)
* fix

* fix

* fix

* more
2025-10-27 22:47:41 -07:00
David
73ed45f9d7 ui screenshots: add screenshot for unifont rendering (#36506)
* ui: add homescreen setup for unifont language setting

* fix params
2025-10-27 20:00:01 -07:00
Shane Smiskol
2d6df2e125 raylib: minor tweaks (#36507)
* try

* generic

* check

* why was this here?!
2025-10-27 19:59:35 -07:00
Shane Smiskol
e754b738ad raylib: fix prime state thread (#36504)
fix
2025-10-27 15:15:48 -07:00
David
1dadb3fcc9 multilang: fix missing translation for longitudinal personality toggle description (#36446)
fix: add translation wrapper for longitudinal personality toggle description
2025-10-27 13:34:35 -07:00
Shane Smiskol
4e88245745 raylib: rename set_callbacks (#36462)
rn
2025-10-27 13:34:16 -07:00
David
debc9bf7cf screenshots: reuse alert setup function (#36473)
ui: refactor onroad alert setup functions for improved reusability
2025-10-27 13:32:15 -07:00
Dean Lee
e03673485b ui: unify cache key in AugmentedRoadView (#36477)
unified cache key
2025-10-27 13:31:55 -07:00
David
6efe4e1998 ci: fix selfdrive_tests weekly run and badge (#36500) 2025-10-27 13:28:27 -07:00
David
ff6ed7055d ci: include assets path for UI label and preview (#36499)
* workflow: include 'selfdrive/assets/**' path for triggering UI preview

* ui: include 'selfdrive/assets/**' path in labeler configuration
2025-10-27 13:28:09 -07:00
Maxime Desroches
6c85e2c697 ModemManager restart (#36433)
* res

* limit

* not needed

* comments + explicit
2025-10-26 18:30:57 -07:00
Adeeb Shihadeh
2d0340cefd ui: stop loading unused fonts (#36493)
Co-authored-by: Comma Device <device@comma.ai>
2025-10-26 14:18:48 -07:00
Adeeb Shihadeh
a974deeb59 ui: replace close button text with icon (#36492) 2025-10-26 14:13:53 -07:00
Adeeb Shihadeh
cf5bb4e16e reduce unifont impact on init time (#36490) 2025-10-26 13:47:47 -07:00
Adeeb Shihadeh
0d4b0ee116 pre-process fonts for raylib (#36489)
* pre-process fonts for raylib

* it's fast!

* raylib processing

* build with scons

* padding

* happy ruff

* all exported

* cleanup

* more pad
2025-10-26 13:20:11 -07:00
Adeeb Shihadeh
03cb3e9dc0 make ruff happy :) 2025-10-26 12:15:34 -07:00
Shane Smiskol
f0dd0b5c8c raylib ui: assert system time valid (#36486)
* assert system time valid

* nl
2025-10-26 00:47:07 -07:00
Adeeb Shihadeh
94ca077e69 ui: add startup profiling (#36482)
* ui: add startup profiling

* lil more
2025-10-25 12:27:01 -07:00
eFini
e92e59ca78 multilang: add gettext package (#36476)
needed for gettext
2025-10-25 09:17:59 -07:00
Shane Smiskol
e0cabc1174 raylib: only load glyphs for unifont (#36475)
* again llm is terrible

* Revert "again llm is terrible"

This reverts commit 423dd289ae5701e2f5bb034efd9329175fc275cc.

* try this
2025-10-24 23:56:59 -07:00
Adeeb Shihadeh
5e2f142704 increase CPU budget 2025-10-24 22:02:25 -07:00
Dean Lee
2beb0ffad1 ui: move INF_POINT to constant (#36470)
move INF_POINT to constant
2025-10-24 22:00:45 -07:00
Dean Lee
fa373af9b5 ui: cleanup .gitignore (#36471)
cleanup .gitignore
2025-10-24 22:00:38 -07:00
Adeeb Shihadeh
7909716c1f ui: realtime scheduling (#36467)
* ui: realtime scheduling

* try this

* update cpu

---------

Co-authored-by: Comma Device <device@comma.ai>
2025-10-24 21:57:45 -07:00
Adeeb Shihadeh
c1cb971bca hardwared: disable power save when screen is on (#36466) 2025-10-24 21:34:37 -07:00
Adeeb Shihadeh
538ec25ad9 gc unused stuff in HW abstraction layer (#36465)
* gc unused stuff in HW abstraction layer

* lil more
2025-10-24 21:07:04 -07:00
Adeeb Shihadeh
17152484c2 selfdrive_tests -> tests 2025-10-24 20:54:13 -07:00
Adeeb Shihadeh
954b567b9b merge a bunch of misc stuff into common.utils (#36463)
just utils
2025-10-24 20:45:56 -07:00
Shane Smiskol
6061476d8e fix spinner (#36458) 2025-10-24 19:43:46 -07:00
Dean Lee
ad903aeaa1 ui: simplify draw_border (#36440)
simplify draw_border
2025-10-24 19:28:58 -07:00
Dean Lee
c8c1b0f781 ui: clear available camera streams after going offroad (#36441)
clear available camera streams after going offroad
2025-10-24 19:27:33 -07:00
Dean Lee
534f096bb8 ui: reset cached height when description changes (#36454)
reset cached height when description changes
2025-10-24 19:27:03 -07:00
David
7da36b2470 multilang: fix missing translations in developer panel (#36445)
fix: translate description strings in DeveloperLayout settings
2025-10-24 19:15:14 -07:00
Dean Lee
f2db7f7665 ui: cache emoji font to avoid repeated loading (#36451)
cache font to avoid repleated loads
2025-10-24 19:13:34 -07:00
David
40a1af97b9 ui: only auto scale on PC if SCALE env not set (#36455)
only use auto scale if SCALE env not set
2025-10-24 19:12:45 -07:00
Dean Lee
53ff5413cd ui: auto-size on PC if screen is smaller than tici (#36452)
auto-scale on PC to fit screen
2025-10-24 08:50:32 -07:00
Maxime Desroches
dc889587ce bump version to 0.10.2 2025-10-23 22:25:54 -07:00
David
6486ab6cab raylib: fix crash when toggling advanced network toggles (#36443)
use get_state()
2025-10-23 15:09:14 -07:00
Shane Smiskol
ab234c72a3 wait slightly longer to take screenshot 2025-10-23 00:55:31 -07:00
Shane Smiskol
485c7b2725 multilib: relative paths (#36439)
* relative

* clean up
2025-10-23 00:54:31 -07:00
Shane Smiskol
4861d15056 reduce ui scons imports 2025-10-23 00:45:51 -07:00
Shane Smiskol
1e73025f86 Remove Qt (#36427)
* rm qt from ui scons

* rm qt translation litter

* rm ccs

* more

* fix cabana

* more

* more

* more
2025-10-22 22:18:07 -07:00
Dean Lee
378212e5ab cabana: remove dependency on selfdrive/ui (#36434)
remove dependency on selfdrive/ui
2025-10-22 21:24:07 -07:00
David
4f52f3f3c5 raylib: match QT colors for danger button style (#36431)
match colors for DANGER style
2025-10-22 19:48:44 -07:00
Shane Smiskol
a0d48b6c63 raylib: unifont for CJK languages (#36430)
* add rest of langs

* unifont

* all langs are supported

* add japanese translations

* fix strip!

* add language name chars

* use unifont in lang selection

* add korean

* test all langs

* doesn't work

* unifont font fallback for multilang

* add ar translations

* fix labels not updating until scrolling

* t chinese

* more chn

* we already default

* wrap

* update

* fix thai

* fix missing chinese langs and all are supported!

* clean up

* update

* ??? mypy r u ok ???

* fix default option font weight
2025-10-22 19:18:54 -07:00
YassineYousfi
b14270bd71 update RELEASES.md 2025-10-22 18:56:18 -07:00
Shane Smiskol
8f720a54f6 raylib: add branch switcher (#36359)
* it's adversarial

* try 2

* just do this

* kinda works but doesn' tmatch

* fine

* qt is banned word

* test

* fix test

* add elide support to Label

* fixup

* Revert "add elide support to Label"

This reverts commit 28c3e0e7457345083d93f7b6a909a4103bd50d55.

* Reapply "add elide support to Label"

This reverts commit 92c2d6694146f164f30060d7621e19006e2fe2df.

* todo

* elide button value properly + debug/stash

* clean up

* clean up

* yep looks good

* clean up

* eval visible once

* no s

* don't need

* can do this

* but this also works

* clip to parent rect

* fixes and multilang

* clean up

* set target branch

* whops
2025-10-22 18:54:09 -07:00
Shane Smiskol
2c41dbc472 raylib: hit rect for scroller items (#36432)
* hit rect

* clean up

* comment

* oh this is actually epic

* rm line

* type
2025-10-22 18:10:32 -07:00
Shane Smiskol
a8660b5b4f Revert "raylib: add branch switcher (#36411)"
This reverts commit 856f8d3d47.
2025-10-22 17:55:28 -07:00
Shane Smiskol
4ccafff123 raylib: multilang (#36195)
* fix multilang dialog height

* split to file

* stash

* Revert "stash"

This reverts commit deb4239fe69f0260420fad03f2350e622e31542f.

* add updater

* add files

* stuff

* try

rev

* stash

* works!

* works!

* this should be the flow?

* cursor wrapping -- it missed entire sections, changed formatting, and didn't use trn properly!!!!!!!!!!!!!!!!!

* update translations

* learned my lesson

* this should be the one thing it's good at

* update trans

* onroad wrap

* spanish

* rename

* clean up

* load all

* Revert "load all"

This reverts commit 6f2a45861c914ffb9d40a5edd15751afd798d614.

* jp translations

* try jp

* Revert "try jp"

This reverts commit d0524b10110104baafcdc1ec385c3d57bc5ef901.

* remove languages we can't add rn

* tr

* pt and fr

* ai cannot be trusted

* ai cannot be trusted

* missing trans

* add fonts

* Revert "remove languages we can't add rn"

This reverts commit 73dc75fae2b9e347d867b6636dab6e2b5fe59da7.

* painfully slow to startup

* only load what we need

* Reapply "remove languages we can't add rn"

This reverts commit 52cb48f3b838520a421f9b90e5ea4409c27d4bd0.

* add system

* that's sick that this just works (dynamic)

* fix description falling back to first str + support callable titles in list items

* device is now live!

* make firehose live

* developer

* network live

* software live

* and that

* toggles live

* regen

* start to clean up gpt

* revert op sans

* bruh

* update translations

* rm old script

* add noops for descriptions to fix translating away from non-english after startup

* missing de

* do filtering in multilang.py

* clean up

clean up

* codespell: ignore po

* fix update

* should not depend

* more live

* sidebar and offroad alert panel live

* fix issues with offroad alerts

* fix firehose live

* fix weird tr("") behavior

* sh key live bugfix

* setup.py live

* update

* update

* no fuzzy matching -- breaks dynamic translations

* rm this

* fix calib desc live trans

* change onroad

* rm dfonts

* clean up device

* missing live

* update

* op lint

* not true

* add to gitignore

* speed up startup by reducing chars by ~half

* fix scons

* fix crash going from qt

* preserve original lang

* cancel kb live translate

* no preserve

* fix lint
2025-10-22 16:28:28 -07:00
Dean Lee
856f8d3d47 raylib: add branch switcher (#36411)
* add branch switcher

* improve
2025-10-22 16:22:51 -07:00
David
00e20f1524 raylib: fix "Reboot" button pressed style (#36412)
use normal style for dual button action left button
2025-10-22 16:19:37 -07:00
Dean Lee
215acefbb4 raylib: precompile regex patterns for faster HTML parsing (#36417)
precompiled regex
2025-10-22 16:18:11 -07:00
Dean Lee
c33c9ff22a raylib: optimize html renderer with height caching (#36418)
optimize html renderer with height caching
2025-10-22 16:17:57 -07:00
Adeeb Shihadeh
99fdd59042 agnos 14.3 (#36426) 2025-10-22 16:11:37 -07:00
Adeeb Shihadeh
5af1099fbf rm watchdog (#36425) 2025-10-22 15:36:09 -07:00
ZwX1616
f983df0c70 camerad: faster exposure convergence at startup (#36424)
* might converge faster

* accept darker at start

* accept darker at start

* it was unreasonably lax
2025-10-22 15:35:58 -07:00
felsager
936740201c latcontrol_torque: refactor low speed factor into pid controller (#36364) 2025-10-22 11:50:37 -07:00
David
4489517eeb keyboard: replace duplicate period key (#36361)
switch between underscore and hypen instead of period
2025-10-22 11:35:58 -07:00
David
b1b7c505a1 raylib: add danger button pressed style (#36413)
add danger hover style
2025-10-22 11:28:37 -07:00
felsager
a2e7f3788f LateralTorqueState: log controller version and desired lateral jerk (#36421) 2025-10-22 10:56:34 -07:00
felsager
d2bb8fe537 latcontrol_torque: more descriptive variable names (#36422) 2025-10-22 10:44:14 -07:00
Maxime Desroches
5289b08bcf bump retry count for micd and soundd (#36415)
retry
2025-10-21 21:54:40 -07:00
ZwX1616
cc8f6eadfe DM: Medium Fanta model 🥤 (#36409)
M fanta: e456b6c5-2dd0-400e-bf0f-6bb5a908971a
2025-10-21 13:58:48 -07:00
Shane Smiskol
9b2f7341d8 raylib: wrap text for multilang (#36410)
* fix multilang dialog height

* split to file

* stash

* Revert "stash"

This reverts commit deb4239fe69f0260420fad03f2350e622e31542f.

* add updater

* add files

* stuff

* try

rev

* stash

* works!

* works!

* this should be the flow?

* cursor wrapping -- it missed entire sections, changed formatting, and didn't use trn properly!!!!!!!!!!!!!!!!!

* update translations

* learned my lesson

* this should be the one thing it's good at

* update trans

* onroad wrap

* spanish

* rename

* clean up

* load all

* Revert "load all"

This reverts commit 6f2a45861c914ffb9d40a5edd15751afd798d614.

* jp translations

* try jp

* Revert "try jp"

This reverts commit d0524b10110104baafcdc1ec385c3d57bc5ef901.

* remove languages we can't add rn

* tr

* pt and fr

* ai cannot be trusted

* ai cannot be trusted

* missing trans

* add fonts

* Revert "remove languages we can't add rn"

This reverts commit 73dc75fae2b9e347d867b6636dab6e2b5fe59da7.

* painfully slow to startup

* only load what we need

* Reapply "remove languages we can't add rn"

This reverts commit 52cb48f3b838520a421f9b90e5ea4409c27d4bd0.

* stash!

* rm

* Revert "stash!"

This reverts commit 31d7c361079a8e57039a0117c81d59bf84f191c7.

* revert this

* revert that

* make this dynamic!

* device

* revert

* firehose

* stuff

* revert application

* back

* full revert

* clean up

* network

* more system

* fix dat

* fixy
2025-10-20 21:39:04 -07:00
Dean Lee
650946cd2a raylib:use context manager for BytesIO (#36407)
use context manager for BytesIO
2025-10-20 19:17:10 -07:00
Shane Smiskol
9801e486d9 fix incorrect Button argument 2025-10-20 18:36:13 -07:00
Shane Smiskol
3381192297 Multilang: remove main prefix (#36406)
* rename

* fix
2025-10-20 18:35:34 -07:00
Harald Schäfer
b2e3dd17ea torque gains not car specific (#36404)
* torque gains not car specific

* remove opendbc interfaces longitudinal control kf field assignment that makes hitl test fail

* typo

* another typo

* bump

* bump openbc

* update ref

---------

Co-authored-by: felsager <d.felsager@gmail.com>
2025-10-20 17:16:03 -07:00
Bruce Wayne
01715f6f9a test car model: use factor for torque 2025-10-20 16:26:22 -07:00
Adeeb Shihadeh
8720e5d712 tools: pass args to op adb 2025-10-20 16:14:02 -07:00
Shane Smiskol
8752093801 raylib: fix option dialog (#36405)
* fix dialog

* rm
2025-10-20 15:45:44 -07:00
YassineYousfi
3c957c6e9d The Cool People's model 😎 (#36249)
* cb8f0d7e-6627-4d7f-ad97-10d0078f2d2c/400

* ci?

* fd9a6816-8758-466b-bbde-3c1413b98f0a/400
2025-10-20 14:09:42 -07:00
Shane Smiskol
3ef5037c16 uploader: fix env var parsing 2025-10-20 11:40:03 -07:00
Harald Schäfer
7534b2a160 PID: no more ff gain (#36398)
* No more ff gain

* typo
2025-10-18 11:12:47 -07:00
Shane Smiskol
b28425b8c3 raylib: fix broken pairing dialog first 5m after startup (#36397)
* always try on dialog show

* except logging

* huge oof
2025-10-17 19:11:12 -07:00
Shane Smiskol
1f5e0b6f68 raylib: show dialog when attempting to pair without internet (#36396)
* match qt

* clean up

* bb

* ofc

* use alert_dialog
2025-10-17 19:00:06 -07:00
David
646f6a1006 raylib screenshots: alpha long toggle confirmation dialog (#36386)
add alpha long toggle confirmation test
2025-10-17 17:27:13 -07:00
Maxime Desroches
cc683f2040 AGNOS 14.2 (#36390)
* version

* env
2025-10-17 02:50:17 -07:00
Maxime Desroches
18e8f648c2 Revert "AGNOS 14.1 (#36389)"
This reverts commit 821e4da2c7.
2025-10-17 01:48:08 -07:00
Maxime Desroches
821e4da2c7 AGNOS 14.1 (#36389)
* stag

* prod
2025-10-17 01:12:55 -07:00
Maxime Desroches
13d98fd2d5 test_onroad: skip more frames for ui timings 2025-10-17 00:36:15 -07:00
Maxime Desroches
92cd656c68 ui: remove watchdog (#36388)
out
2025-10-17 00:26:29 -07:00
Maxime Desroches
727a750b34 ci: stop power_monitor once 2025-10-16 23:37:44 -07:00
Maxime Desroches
5dabb678ce ci: just stop power_monitor on devices 2025-10-16 23:35:31 -07:00
Maxime Desroches
ef988aca28 raylib: bump version 2025-10-16 23:23:39 -07:00
Maxime Desroches
64f3759fd0 cleanup release branches 2025-10-16 15:07:45 -07:00
Shane Smiskol
d71d2bd2d0 test_onroad: ignore first few ui timing frames (#36385)
clean up
2025-10-16 03:45:50 -07:00
Shane Smiskol
702bebf176 raylib: fix temporarily untoggleable onroad experimental mode button (#36383)
* gpt got it after 2 tries, but still not immed mergeable

* bad bot
2025-10-16 02:22:49 -07:00
Shane Smiskol
25da8e9d44 raylib: fix crash from too many colors (#36382)
* fix

* bump
2025-10-16 02:22:02 -07:00
Maxime Desroches
845f6ec8cf build new staging branch 2025-10-16 02:06:09 -07:00
Maxime Desroches
e1ad4daf8d installer: branch migration (#36315)
* mig

* fix

* fix

* more

* staging
2025-10-16 01:59:23 -07:00
Maxime Desroches
783b717af8 AGNOS 14 (#36313)
* version

* updater

* this order

* manifest

* update

* prod

* logic

* magic

* new

* bump

* bump

* new

* b

* bump

* prod
2025-10-16 00:49:05 -07:00
Shane Smiskol
65e1fd299e raylib: fix full size alert text (#36379)
* stash so far

* try this

* better

* fast

* rename

* revert

* clean up

* yes

* hack to make it work for now

* actually fix

* fix
2025-10-15 22:53:37 -07:00
Shane Smiskol
b29b1964ba raylib screenshots: test onroad (#36369)
* test onroad

* person

* onroad alert

* mid and full

* all

* can do this

* tf

* tf

* clean up
2025-10-15 22:10:55 -07:00
Shane Smiskol
80a8df0643 raylib: fix emoji centering with Label (#36376)
* kinda works

* but spacing was off, so back to big emoji

* rm debug

* fixed!

* fixed!

* fix newline in emoji pattern

* fix

* fix dat
2025-10-15 21:53:52 -07:00
Shane Smiskol
d9fc6c0086 raylib: small Label clean up (#36377)
* do

* clean up

* text raw is the default!
2025-10-15 21:11:00 -07:00
Shane Smiskol
cb612a4b90 raylib: no Label padding (#36374)
* none

* try this

* fix

* stash

* remove text padding from label, but keep for button

* simpler is to default to 0

* fix
2025-10-15 20:13:42 -07:00
Shane Smiskol
36d77debd0 raylib: remove redundant text center enum (#36372)
* rm

* type

* fix

* fix
2025-10-15 19:32:13 -07:00
Maxime Desroches
530ad2925d ui: clean raylib even on SIGINT (#36368)
* fix

* keep

* fix
2025-10-15 17:03:49 -07:00
Shane Smiskol
ec33519dc7 raylib: revert 0 button padding (#36360)
* back to 20

* here only
2025-10-15 01:38:00 -07:00
Shane Smiskol
2fd4b53aaf raylib: smooth path distance (#36278)
* smooth max distance

* junk

* clean up

* final

* you can read

* Update selfdrive/ui/onroad/model_renderer.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* true

* fix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-15 00:48:05 -07:00
Shane Smiskol
a2c4fe1c90 raylib: fix setup styles (#36322)
* hardcoding is bad for you

* do updater

* reset

* lint

* duh!

* fixup setup

* fixup updater

* unround
2025-10-15 00:11:16 -07:00
Shane Smiskol
3e56612990 raylib: fix emoji vertical centering (#36358)
* space

* font scale

* fix centering
2025-10-15 00:06:04 -07:00
Shane Smiskol
75858673c4 less rounded border 2025-10-15 00:01:52 -07:00
Shane Smiskol
57223958b5 raylib screenshots: add more homescreen states (#36356)
* hmm can do yeidl approach

* clean up

* clean up

* flip

* and add paired + prime

* sort and add update params

* try

* all should have branch name

* test

* clean up

* add offroad alert to update screen
2025-10-14 23:56:15 -07:00
Shane Smiskol
3553a754a4 Fix vendored emoji font (#36357)
* add font

* use it

* rm old one
2025-10-14 22:45:33 -07:00
David
f290fb1e05 keyboard: fix double space (#36345)
* support multiple characters added add cursor position

* fix

* remove double space

* Revert "fix"

This reverts commit c938a52995b6f5343b461f408af5838b78f453d2.

* Revert "support multiple characters added add cursor position"

This reverts commit d8225a768686a88f2bdaabae6d2a57c541ac7f77.
2025-10-14 22:15:31 -07:00
David
0c64818f52 Add screenshot for advanced network settings (#36351)
add screenshot for advanced network settings
2025-10-14 21:36:04 -07:00
ZwX1616
c44548ba0f camerad: make wide brightness more consistent with road (#36355)
align
2025-10-14 21:27:52 -07:00
Shane Smiskol
59bddfba8d raylib: rounded onroad corners (#36348)
* rounded corners

* use scissor

* 0.1

* middle

* don't trust chatter

* round

* clean p

* cleanup

* rev
2025-10-14 21:24:36 -07:00
Shane Smiskol
8a1fcd8991 raylib: fix possible DM crash (#36354)
* fix

* bruh

* clean up

* here

* rm
2025-10-14 21:17:36 -07:00
felsager
3546b625e7 latcontrol_torque: change in kp should not affect effective low speed factor gain (#36335) 2025-10-14 13:22:17 -07:00
Shane Smiskol
87443cd34d raylib: background onboarding texture loading (#36343)
* this seems best so far

* better

* clean up

* debug

* debug

* clean up

* final
2025-10-14 00:45:03 -07:00
Shane Smiskol
5f0e9fce61 raylib: update experimental mode homescreen button (#36344)
* update homescreen exp mode button

* and here

* Apply suggestions from code review
2025-10-14 00:40:41 -07:00
Shane Smiskol
a2cce7f897 raylib: fix border radius (#36346)
* colors

* revert color

* rev
2025-10-14 00:39:22 -07:00
Shane Smiskol
f4041dc1f0 raylib: black sidebar (#36347)
black sidebar
2025-10-14 00:33:38 -07:00
Shane Smiskol
8e3757ac87 raylib: image dimensions are optional (#36332)
* meas

* now no resizing

* clean up
2025-10-13 16:49:39 -07:00
David
41fa0cdf82 fix cycle offroad alerts (#36302)
* fix: use parse_release_notes in cycle_offroad_alerts

* fix: set update available param true

* Update selfdrive/ui/tests/cycle_offroad_alerts.py

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-10-13 14:07:11 -07:00
David
692f5fdd72 Add screenshot for experimental mode description (raylib UI test) (#36327)
* feat: add test case to expand experimental mode description

* Update selfdrive/ui/tests/test_ui/raylib_screenshots.py

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-10-13 13:56:47 -07:00
Shane Smiskol
de0a1e66d8 software download screenshot (#36326)
* software

* clean up

* Qt ButtonControl has 0 padding

* clean up

* clean up
2025-10-13 13:54:50 -07:00
Maxime Desroches
129445cd1d setup: don't wait for so long (#36323)
nice
2025-10-12 00:37:36 -07:00
Shane Smiskol
13d0aefd7c raylib: don't get old onroad alert on startup 2025-10-11 23:40:42 -07:00
Shane Smiskol
5f7b05e808 raylib: don't create vipc client twice first time 2025-10-11 23:36:05 -07:00
Shane Smiskol
32f65bae55 alpha long: allow toggle while onroad + restart op
it's alpha, and some cars don't fault (we allow other toggles which would fault, so why not enable)
2025-10-11 23:29:08 -07:00
Maxime Desroches
49d9b8bb00 ui: fix cloudlog spam (#36321)
* dark office

* check

* back

* fix

* remove

* remove
2025-10-11 23:06:06 -07:00
Shane Smiskol
b3eba70b7a raylib: flip! (#36319)
* flip!

* add ui

* ?

* qt is extra

* low node

* add uiDebug

* fix

* fix dat

* bump double increase for tol

* it's ~11ms but double for tol

* fix report

* Update selfdrive/test/test_onroad.py
2025-10-11 07:41:29 -07:00
Shane Smiskol
cec7a5dc98 raylib: fix styling for fullscreen alerts (#36318)
* fix that

* fix styling

* and this

* revert

* fix full

* revert
2025-10-11 06:55:35 -07:00
Shane Smiskol
14993f58e3 raylib: set speed fixes (#36317)
* remove msaa artifacting by heavily reducing segments and match radius

* always draw set speed with '-' like qt

* clean up

* match qt behavior for rivian
2025-10-11 06:28:49 -07:00
Shane Smiskol
e8a17b4963 raylib: fix stale frames going onroad (#36314)
* fix

* try

* flip

* now flip

* fix network nav button heights

* revert
2025-10-11 06:04:27 -07:00
Shane Smiskol
fb77212221 raylib: add more spacing to network nav buttons 2025-10-11 06:02:10 -07:00
Shane Smiskol
aa7f6973c0 raylib: match Qt onroad button disabling (#36316)
* fixxx

* clean up

* disable onroad: adb, joystick, alpha long
2025-10-11 06:01:17 -07:00
Maxime Desroches
c2af5a82ff add earcut python package 2025-10-11 04:29:05 -07:00
Shane Smiskol
348114e5bd raylib: remove cut stuff (#36310)
remove cut stuff
2025-10-11 04:27:19 -07:00
Shane Smiskol
a6e28ac2ee raylib: disable mouse thread lag print (#36312)
every other process disables this
2025-10-11 04:27:01 -07:00
Maxime Desroches
0e6f78a656 raylib is now a core dependancy 2025-10-11 04:23:16 -07:00
Shane Smiskol
2305fb59a2 raylib: fix mic indicator (#36309)
* fix mic

* move out
2025-10-11 03:40:40 -07:00
Shane Smiskol
cc816043c1 raylib: add non-inline b tag (#36305)
* debug

* here too

* clean up
2025-10-11 03:36:47 -07:00
Shane Smiskol
b6dbb0fd8d raylib: font sizes from QT should match (#36306)
* pt 2

* fix line height

* fixup html renderer

* fix sidebar

* fix label line height

* firehose fixups

* fix ssh value font styling

* fixup inputbot

* do experimental mode

* pairing dialog numbers

* fix radius for prime user

* add emoji to firehose mode

* full screen registration

* fix registration btn size

* fix update and alerts

* debugging

* Revert "debugging"

This reverts commit 0095372e9479d8c727bcc8a78061f582d852133d.

* firehose styling

* fix offroad alerts missing bottom spacing expansion

* huge oof

* huge oof
2025-10-11 03:29:24 -07:00
Shane Smiskol
fdcf8b592e raylib: reset scrollers and description expansions on show event (#36304)
* scroll up on hide

* switch to show

* dismiss descriptions too!

* all is show

* all is show

* clean up

* visible items helper

* Revert "visible items helper"

This reverts commit e64f05b69155483aa0f3d74bd511f5d7c1ecfb79.

* reset
2025-10-10 21:07:36 -07:00
Shane Smiskol
4ff77a4752 raylib: fix DMoji (#36301)
* wow first time cursor made a tiny change to fix a problem!

* rm
2025-10-10 04:10:24 -07:00
Shane Smiskol
f04ee80452 raylib: implement calibration description (#36300)
* this is all cursor

* also cursor

* inline reset calib

* calib desc

* way better

* huh

* clean up

* rcvr

* stash changes to change params

* Revert "stash changes to change params"

This reverts commit ee998f04c4235ed20493b83e35c9f28e182d89b0.
2025-10-10 03:57:12 -07:00
Shane Smiskol
ddbbcc6f5d raylib: add experimental mode + alpha long confirmation dialog + related fixes (#36295)
* here's everything

* just the dev part

* same for exp mode!

* use rich

* fix br not working in p

* html height needs to be different than content/text rect

* fix confirmation

* fix

* fix 2.5s lag

* clean up

* use correct param

* add offroad and engaged callback too

* nl

* lint
2025-10-10 03:03:35 -07:00
Shane Smiskol
0f40afa357 raylib: fix black updater bg for network (#36299)
fix black bg
2025-10-10 01:41:11 -07:00
Shane Smiskol
cac8d3f405 raylib: fix missing showing dialog in setup/updater (#36298)
* fix showing dialog

* here for safety
2025-10-10 01:40:29 -07:00
Shane Smiskol
b521a913ab raylib: confirm dialog uses HTML renderer 2 (#36297)
* start

* keep it simple

* rm
2025-10-10 01:18:07 -07:00
Shane Smiskol
d6651ccd82 raylib: implement developer panel (#36292)
* first pass by cursor

* fix

* tell it what's good

* stash

* desc

* clean up junk

* alpha long can't use onroad cycle again due to faults

* lint

* fix kb
2025-10-09 23:09:55 -07:00
Shane Smiskol
2976798852 raylib: implement toggles (#36284)
* start on exp mode

* more

* fmt

* rm

* 2nd try

* almost there

* clean up

* and this

* fmt

* more

* exp is colored when active

* move out, and rm redudnant self.state

* revert html changes for now

* fix untagged text inheriting previous tag

* why would this be unknown

* here too

* update live with car

* clean up + refresh toggles on showEvent + catch from cursor about setting desc if no carparams

* not sure why

* fix disengaged re-enabling locked toggles
2025-10-09 19:50:27 -07:00
Shane Smiskol
1b90b42647 Html renderer: reset tag so untagged text doesn't inherit last tag 2025-10-09 19:10:10 -07:00
felsager
de805e4af7 Lateral torque controller: use measurement rate as error rate (#36291) 2025-10-09 14:57:12 -07:00
YassineYousfi
4d085424f8 North Nevada Model 🏜️ (#36276)
* e2d9c622-25a8-4ccd-8c8e-c62537b7aa0c/400

* 0e620593-e85f-40c2-9adf-1e945651ed13/400
2025-10-09 12:58:27 -07:00
felsager
d07981ea3c bump opendbc (#36289) 2025-10-09 11:48:13 -07:00
felsager
22d5cbd0fa PID: coefficients should be in front, i_rate should be i_dt (#36288) 2025-10-09 11:10:44 -07:00
felsager
4c9ca91b98 Latcontrol: use more accurate naming for saturation time (#36286) 2025-10-09 10:34:26 -07:00
felsager
0736f325fc Latcontrol torque: cleaner low_speed_factor calculation (#36287) 2025-10-09 10:29:35 -07:00
kostas.pats
dcc5afa8fa improve webrtc stack for use in camera focusing (#36268)
* made LiveStreamVideoStreamTrack use system time to calculate pts

* fixes as requested

* Align panda submodule with master (panda@615009c)

* made loggerd accept a run time env variable to pick stream bitrate

* added /notify endpoint to send json to all session's data channel

* fixed static analysis error

* adapted webrtc stream test to new pts calculation method

* fixed static erro

* fixed wrong indent

* fixed import order

* delete accidental newline

* remove excess spaces

Co-authored-by: Maxime Desroches <desroches.maxime@gmail.com>

* remove excess spaces

Co-authored-by: Maxime Desroches <desroches.maxime@gmail.com>

* changed exeption handling based on review

* fixed typo on exception handling

---------

Co-authored-by: Maxime Desroches <desroches.maxime@gmail.com>
2025-10-08 22:30:32 -07:00
felsager
226465e882 Latcontrol: refactor pid error to factor out lateral jerk component (#36280) 2025-10-08 18:29:54 -07:00
Shane Smiskol
0b62dbe16b raylib: more closely match Qt alert sizes (#36283)
* hmm this doesn't work

* clean up

* more

* bad fmtr

* match sidebar net texts

* better
2025-10-08 17:29:45 -07:00
felsager
2deb4e6f65 Lateral controllers: pass dt (delta time) explictly (#36281) 2025-10-08 14:39:05 -07:00
felsager
9f32f217e6 Latcontrol: type annotate update inputs and clip_curvature output (#36282) 2025-10-08 14:26:53 -07:00
Shane Smiskol
e62781cccb Revert "raylib: font sizes from QT should match (#36237)"
This reverts commit 7933c10c97.
2025-10-08 04:05:49 -07:00
Shane Smiskol
e1912fa5be raylib: speed up polygon shader (#36275)
* actually works

* fix shader grad

* switch

* our own triangulate

* this is amazing

* ok 100 is too much for 3x. 10?

* fix colors

* review intern chad

* fmt

* rm for the line count

* bye

* rm

* see the diff

* start to revert nulleffect

* fix

* fix

* always feather

* aliasing doesn't seem necessary

* aliasing doesn't seem necessary

* fix lane lines disappearing halfway up due to buggy deduping -- very simple triangulation function takes ~same CPU time + same GPU utilization on PC (nvidia-smi)

* remove old

* even simpler triangulate

* this is useless

* more revert

* split color out again

* clean up ai bs

* back to original names

* more clean up

* stop it

* this limiting logic split out feels more even // less super dense

* typing

* clean up a little

* move to get grad color

* RM

* flip

* document

* clean up

* clean up

* clean

* clean up

* not a "state"

* clean up

* that did nothing

* cmt
2025-10-08 03:51:37 -07:00
Maxime Desroches
a7fe9db773 fix installer build 2025-10-06 16:37:50 -07:00
Shane Smiskol
35296a8692 flip setting order (#36266)
flip
2025-10-06 00:56:13 -07:00
nayan
9d7193c0e7 driving stats 2025-10-05 19:18:10 -04:00
nayan
87718a3c21 add Display panel 2025-10-05 14:42:52 -04:00
nayan
5d96be11de Network panel fix 2025-10-05 14:30:44 -04:00
nayan
daf2060324 Merge remote-tracking branch 'origin/master' into nayan-raylib
# Conflicts:
#	common/params_keys.h
2025-10-05 14:14:50 -04:00
Maxime Desroches
31801a7312 no more wayland for installer 2025-10-04 02:47:25 -07:00
Maxime Desroches
cc7ecd53c7 raylib: bump commit 2025-10-04 02:45:40 -07:00
Shane Smiskol
586e49cab3 Revert "Switch to raylib for UI (#36238)"
This reverts commit c88ab5cd12.
2025-10-04 01:04:20 -07:00
Shane Smiskol
ebe47a580c raylib: fix registration box height 2025-10-04 01:04:05 -07:00
Shane Smiskol
7933c10c97 raylib: font sizes from QT should match (#36237)
* debug

* hacks everywhere but kind of works

* by font

* fix sidebar

* stash

* test update

* just use a const

* just use a const

* better

* clean up

* fix label

* simplify

* gpt5 is yet again garbage

* rm that

* clean up

* rm

* blank

* clean up

* I really don't like this but shrug

* fix

* fix experimental text
2025-10-04 00:32:49 -07:00
Shane Smiskol
2bc97ee23f raylib: fix DM popup (#36265)
* come on

* try

* better

* better

* multiple places!

* debug

* works

* temp

* whoops

* wonder if this wortks

* ah need this!

* wtf is this when deleted?

* another day no modal show event

* clean

* fix

* ugh

* need this
2025-10-04 00:05:20 -07:00
Shane Smiskol
c88ab5cd12 Switch to raylib for UI (#36238)
* flip

* change this
2025-10-03 23:38:10 -07:00
Shane Smiskol
943aaef76a raylib: match Qt onroad alert colors (#36264)
fix alert colors
2025-10-03 23:32:17 -07:00
Shane Smiskol
3fd9e94a34 raylib: all system apps work without anything built (#36261)
* all system apps work without scons

* better

* fix

* revert

* fix

* dont add

* huh
2025-10-03 23:18:20 -07:00
Shane Smiskol
e423f8f605 raylib: elide version on homescreen (#36263)
* elide ver on hom

* rm line

* blank
2025-10-03 23:17:51 -07:00
Shane Smiskol
0eb90ecb3e raylib: elide list item actions (#36262)
fix
2025-10-03 23:04:55 -07:00
Maxime Desroches
703f3d0573 disable sim test for now 2025-10-03 22:09:00 -07:00
Shane Smiskol
2337704602 raylib: release notes are drawn with HTML renderer (#36245)
* stash

* ok chatter is useful for once

* draw text outside tags

* hmm

* undo that shit

* i don't like this chatgpt

* Revert "i don't like this chatgpt"

This reverts commit 5b511911d81242457bfb5fc808a9b9f35fe9f7a2.

* more robust parsing (works with missing tags, markdown.py actually had bug) + add indent level

* the html looks weird but is correct - the old parser didn't handle it

* clean up

* some

* move out

* clean up

* oh this was wrong

* draft

* rm that

* fix

* fix indentation for new driving model

* clean up

* some clean up

* more clean up

* more clean up

* and this

* cmt

* ok this is egregious mypy
2025-10-03 21:47:53 -07:00
Shane Smiskol
bd9888a439 raylib screenshots: add software release notes (#36259)
add software
2025-10-03 21:29:20 -07:00
Shane Smiskol
12b3d0e08d raylib: cache wrap text (#36258)
* cache html height

* clean up

* todo
2025-10-03 20:52:50 -07:00
Shane Smiskol
89d350a791 raylib html renderer: fixups (#36257)
* this wasn't used

* override text size and color

* render untagged text as paragraph

* and indent

* cache expensive height calc

* fmt

* fix that

* unclear if this is even needed

* and that

* huh

* debug

* Revert "debug"

This reverts commit 7d446d2a37a96e6bd1001c566d4f8e8f417f8fb7.
2025-10-03 20:42:42 -07:00
Shane Smiskol
99a83e5522 Revert "raylib screenshots: find diff faster (#36255)"
This reverts commit a8328cb5ff.
2025-10-03 20:35:32 -07:00
Armand du Parc Locmaria
4d53a26a06 relock after inplace metadrive update (#36256)
* relock after inplace metadrive update

* Revert "relock after inplace metadrive update"

This reverts commit 18193ffe34b66085e18605e6c9289ddcd658844d.

* just the hash
2025-10-03 19:43:03 -07:00
Shane Smiskol
a8328cb5ff raylib screenshots: find diff faster (#36255)
* ?

* run it

* wrong

* here too

* revert
2025-10-03 17:59:42 -07:00
Shane Smiskol
844c328625 raylib screenshots: prevent saving black frame
raylib screenshots: prevent saving black frame
2025-10-03 17:59:19 -07:00
Shane Smiskol
39b97d4e18 raylib screenshots: use long branch name (#36254)
* stress test

* everything
2025-10-03 17:50:02 -07:00
Shane Smiskol
45f497e8f6 raylib screenshots: add mouse click helper (#36253)
* add helper

* rm

* name

* fix
2025-10-03 17:46:39 -07:00
Shane Smiskol
edc5a0412c rename to setup_settings 2025-10-03 17:35:10 -07:00
Shane Smiskol
9670e3a5eb raylib: add confirmation dialog (#36252)
* conf

* update case

* fix

* fix

* rm

* back

* alread setup

* avail
2025-10-03 17:34:53 -07:00
Shane Smiskol
7b2b10bc9e raylib screenshots: raise ui delay 2025-10-03 17:34:22 -07:00
YassineYousfi
bd357adb8b update release notes for 0.10.1 2025-10-03 17:01:20 -07:00
Shane Smiskol
670b6011da raylib: match QT confirmation dialog size (#36248)
* closer to qt

* this too

* eval
2025-10-03 16:13:40 -07:00
Armand du Parc Locmaria
150ff72646 Dockerfile.openpilot: don't set UV_PROJECT_ENVIRONMENT (#36246)
* Dockerfile.openpilot: don't uv sync with root

* Revert "Dockerfile.openpilot: don't uv sync with root"

This reverts commit 2c271d0b5b55d6ae2ece6b28dc90a96e6e891ded.

* don't set UV_PROJECT_ENVIRONMENT
2025-10-03 14:36:12 -07:00
Shane Smiskol
d567442136 raylib: split out HTML renderer (#36244)
* stash

* ok chatter is useful for once

* why doesn't it understand?!

* rm that

* clean up
2025-10-03 00:37:47 -07:00
Shane Smiskol
540fff5226 raylib: draw update button and fix incorrect font (#36243)
* always update layout rects

* don't ever use raylib font

* use it

* such as
2025-10-03 00:20:30 -07:00
Shane Smiskol
21273c921e raylib: excessive actuation offroad alert (#36242)
* excessive actuation check

* from gpt

* back

* use buttons

* use widgets for ultimate clean up - no ai slop

* feature parity

* revert

* clean up
2025-10-02 23:54:31 -07:00
Shane Smiskol
75e52427d1 raylib: fix when we show offroad alerts and styles (#36240)
* fix how we show alerts

* test this too

* match border radius

* simplifty

* keep

* back

* fix alert spacing

* fix alert text padding

* cmt

* cmt
2025-10-02 22:53:02 -07:00
Shane Smiskol
21fd3d0320 raylib: use extra text in offroad alerts (#36241)
* replace properly

* test
2025-10-02 21:21:50 -07:00
Shane Smiskol
1ee798439a raylib: WiFi fixes (#36239)
* proces in AN and WM

* clean

* ban api

* fix

* fiix

* fix pairing dialog

* cleanup

* fix multi action button hard to click

* fix

* fix right margin of multi action

* clean up
2025-10-02 21:09:17 -07:00
Armand du Parc Locmaria
cc52f980b3 Dockerfile.openpilot uv run scons (#36236)
* Dockerfile.openpilot_base use UV_PROJECT_ENVIRONMENT

* Revert "Dockerfile.openpilot_base use UV_PROJECT_ENVIRONMENT"

This reverts commit 3725e54ce0727077ca4347d24ca38e25d5864d47.

* Reapply "Dockerfile.openpilot_base use UV_PROJECT_ENVIRONMENT"

This reverts commit 11b04f57acb9c81fcc5a22a6a6d78d666c59ca6c.

* use uv run to pick up correct ppath
2025-10-02 15:31:39 -07:00
Shane Smiskol
ec7e3192bb revert that 2025-10-02 04:00:09 -07:00
Shane Smiskol
3fd352a7ef raylib: updater UI (#36235)
* auto attempt

* gpt5

* Revert "gpt5"

This reverts commit 556d6d9ee4d53aca0f4612023db6cfb2bed7ce29.

* clean up

* fixes

* use raylib

* fixes

* debug

* test update

* more

* rm

* add value to button like qt

* bump

* bump

* fixes

* bump

* fix

* bump

* clean up

* time ago like qt

rm

* bump

* clean up

* updated can fail to respond on boot leading to stuck state

* fix color

fix

* bump

* bump

* add back

* test update

* no unknown just ''

* ffix
2025-10-02 03:57:10 -07:00
Shane Smiskol
49570c11c6 Remove animation from networking 2025-10-02 02:04:09 -07:00
Shane Smiskol
b8ae62a0b1 raylib scroll panel: check bounds (#36233)
check in bounds rect for scroll panel!!
2025-10-01 01:02:35 -07:00
Shane Smiskol
29a6f0504a raylib: fix WiFi in setup and updater (#36232)
move back to more base class
2025-10-01 00:46:51 -07:00
Shane Smiskol
eadab06f59 raylib: remove gui_button (#36229)
* vibing can be good

* and listview

* rm that

* html render

* text.py

* ssh keys

* updater w/ Auto

* wow gpt5 actually is better

* well this is better

* huh wifi still doesn't work

* lfg

* lint

* manager waits for exit

* wait a minute this changes nothing

* this will work

* whoops

* clean up html

* actually useless

* clean up option

* typing

* bump
2025-10-01 00:32:09 -07:00
Shane Smiskol
9493f2a0eb raylib: remove functional confirmation dialog (#36231)
* rm

* yess

* clean up
2025-09-30 23:48:04 -07:00
Shane Smiskol
b593b7cc43 raylib: SSH key text entry works more than once (#36230)
* impossible

* jarn

* actually space

* forgot
2025-09-30 23:43:23 -07:00
Shane Smiskol
5c0c2a17b0 raylib: add mic indicator (#36207)
* update lang

* mic indicator

* clean up

* clean up

* switch

* fix

* revert
2025-09-30 22:30:45 -07:00
Shane Smiskol
5f33b2fb2d raylib: frame independent scroller (#36227)
* rm that

* almost

* yess

* some work

* more

* todo

* okay viber is good once in a while

* temp

* chadder can't do this

* revert

* this was broken anyway

* fixes

* mouse wheel scroll

* some clean up

* kinda works

* way better

* can tap to stop

* more clean up

* more clean up

* revert last mouse

* fix

* debug only

* no print

* ahh setup.py fps doesn't affect DEFAULT_FPS ofc

* rest

* fix text

* fix touch valid for network
2025-09-30 22:25:43 -07:00
Shane Smiskol
63e0e038fa raylib: don't use DEFAULT_FPS (#36228)
* dont use DEFAULT_FPS

* replace
2025-09-30 22:11:21 -07:00
ZwX1616
d24a14cb39 DM: Large Donut model 🍩 (#36198)
* 59cfd731-6f80-4857-9271-10d952165079/225

* deprecate at the end
2025-09-30 20:32:19 -07:00
Shane Smiskol
3efa52f53b fix missing import 2025-09-30 20:05:40 -07:00
Shane Smiskol
16a4206720 Revert "Reapply "raylib: 20 FPS onroad (#36208)""
This reverts commit ed185e90f6.
2025-09-30 16:34:45 -07:00
Maxime Desroches
e4784d44f6 bump panda (#36226)
bump
2025-09-30 14:33:10 -07:00
Shane Smiskol
aaf2aac050 raylib: training guide (#36224)
* fix regulatory

* debug slow loading

* easy gather step coords

* gotcha

* and fix

* dm option

* fix final

* fixes

* progress bar!

* "vibe coding is great"

* wtf gpt5

* jfc

* hand crafted >> vibe

* it's slow so only load images if we're doing any kind of training

* tf

* format

* clean up

* clean up

* no float

* cmt

* more clean up

* clean up

* eww

* rm

* no debug

* match y

* clean that up

* here too

* windows
2025-09-30 03:11:42 -07:00
Shane Smiskol
b5ec0e9744 raylib: fix regulatory 2025-09-30 02:39:19 -07:00
Shane Smiskol
070a13096b raylib: add todo for niceness (#36210)
* not nice

* hmm

* debug

* todo

* revert

* yep
2025-09-29 13:03:03 -07:00
commaci-public
7ccab2bdb9 [bot] Update Python packages (#36220)
* Update Python packages

* revert tg, model diff looks a bit fishy

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-09-28 13:50:13 -07:00
Shane Smiskol
e9434befaa Refactor offroad alerts loading to use OFFROAD_ALERTS (#36214)
* Refactor offroad alerts loading to use OFFROAD_ALERTS

* clean up
2025-09-27 03:37:23 -07:00
Shane Smiskol
56c77fd5fa re-run 2025-09-27 03:32:19 -07:00
Shane Smiskol
e6bd88371e fix! 2025-09-27 02:55:40 -07:00
Shane Smiskol
bc30b01eb7 Fix raylib ui report (#36215)
hmm
2025-09-27 02:53:21 -07:00
Shane Smiskol
ef93981bfa raylib: ui diff test (#36213)
* add raylib ui test

* match qt

* exe

* vibing is epic

* this is epic

* format

* add more settings

* fix to actually use raylib

* add kb

* global

* pair

* rm cmts

* show event

* this is so stupid

clean up

* clean up

* rename dir

* clean up

* no more vibe

* rm

* ugh it's always slightly different for no reason

* nvm region is actually broken

* 1l
2025-09-27 02:37:35 -07:00
Shane Smiskol
35e2fc7dd9 raylib: use touch thread in all places (#36212)
* fix not opening alerts

* whops

* rm mouse pressed from offroad alerts

* ah its a base class

* one last place

* fix

* rm lines
2025-09-26 23:49:17 -07:00
Shane Smiskol
2feddf32b2 raylib: fix lost onroad tap events (#36211)
* debug

* see it's good to have abstraction

* clean up

* fine

* wtf do you mean mypy? how can you not coerce this?
2025-09-26 22:40:38 -07:00
Shane Smiskol
ed185e90f6 Reapply "raylib: 20 FPS onroad (#36208)"
This reverts commit 5cbfc7705b.
2025-09-26 21:43:15 -07:00
Shane Smiskol
19fc66f88a Fix tearing offroad 2025-09-26 21:41:51 -07:00
Shane Smiskol
5cbfc7705b Revert "raylib: 20 FPS onroad (#36208)"
This reverts commit 8de8c3eb00.
2025-09-26 21:41:12 -07:00
Shane Smiskol
8de8c3eb00 raylib: 20 FPS onroad (#36208)
* 20

* dynamic fps

* flip

* init to be safe

* fix possible fps weirdness

* gate on change

* not now

* rev
2025-09-26 21:38:59 -07:00
Shane Smiskol
04365f12ff raylib: remove unused globals 2025-09-26 21:10:09 -07:00
Shane Smiskol
9297cd2f3e raylib: use filter for allow throttle (#36209)
* use time here

* use epic filter

* rm

* intermediary

* tune
2025-09-26 21:08:39 -07:00
Shane Smiskol
0711160b1c raylib: dismiss dialog on pair (#36205)
* show for unknown

* use Button to make clicking work

* close on pair

* close on pair

* make widget!

* dynamic pairing btn

* whyyy

* clean up

* can do this

* this button is also hard to tap
2025-09-26 18:59:15 -07:00
Shane Smiskol
33f01084d1 raylib: implement cell settings (#36204)
* get vibing

* simplify

* vibing is bad

* simplify

* fix that

* now update

* clean up

* last two

* cell is UpdateUnsaved so we don't need to disable

* we only need actions

* we only need actions

* sort

* stuff

* dont deactivate

* clean up

* clean up

* more

* ipv4 fwd

* warns

* fixz

* rm

* clean up

* one return point

* format

* top
2025-09-25 23:44:12 -07:00
Adeeb Shihadeh
1fbec6f601 remove .clang-tidy 2025-09-25 21:02:26 -07:00
Adeeb Shihadeh
cf5b743de6 build system cleanups (#36202)
* it's all common

* never getting fixed

* it's just tici

* reorders

* qcom2 -> tici

* Revert "qcom2 -> tici"

This reverts commit f4d849b2952cb0e662975805db6a1d32511ed392.

* Reapply "qcom2 -> tici"

This reverts commit 58b193cb8de872830f8a7821a339edca14e4a337.

* is tici

* lil more

* Revert "is tici"

This reverts commit a169be18d3fdcb3ef8317a63a89d8becadabfad8.

* Revert "Reapply "qcom2 -> tici""

This reverts commit 26f9c0e7d068fc8a1a5f07383b3616e619cd4e8c.

* qcom2 -> __tici__

* lil more

* mv lenv

* clean that up

* lil more]

* fix

* lil more
2025-09-25 20:55:14 -07:00
Shane Smiskol
2c377e534f raylib: wifi manager initialize function (#36203)
* init func

* rm print

* rm

* use get_conn settings in another place
2025-09-25 20:48:12 -07:00
Shane Smiskol
1ca9fe35c2 raylib: networking parity with QT (#36197)
* match style

* all this was not naught

* cool can do this

* fix toggle callback - also not for naught

* always process callbacks

* toggle stuff

* cleaner

* tethering password

* clean up

* todos for later

* this is fineee

* add metered options

* wifi metered button

* add hidden network buutton and fix instant modal to modal

* damped filter

* Revert "damped filter"

This reverts commit f9f98d5d708fb15cf1ebef4bdace577f0e347658.

* fix metered toggle when disconnected

* fix tethering enabled

* ohh

* fix keyboard title

* disable edit button temp

* move here

* proper disable

* clean up

* more

* move for loop into enqueue function

* flippy

* got more :(

* todo

* clean up

* mypy

* rename

* todo

* rename

* again

* again

* format
2025-09-25 20:16:14 -07:00
Adeeb Shihadeh
56c49b3b42 cleanup dead build flags 2025-09-25 19:28:16 -07:00
Shane Smiskol
5429748767 raylib: fix button clicking on device (#36201)
* fix button clicking on device

* clean up
2025-09-25 19:27:13 -07:00
Greg Hogan
6aecf59536 add ssh hostname comma- prefix for convenience (#36199) 2025-09-25 17:42:11 -07:00
Shane Smiskol
afc7ff1b7a raylib: fix multilang dialog height (#36196)
* fix multilang dialog height

* clean up
2025-09-24 17:14:06 -07:00
Jason Young
222e880561 Honda: Add 2021 Acura TLX to release (#36193)
* bump opendbc

* regen CARS.md

* add to RELEASES.md
2025-09-24 15:24:15 -04:00
Maxime Desroches
6901e3417b add 3X release branch to RELEASE_BRANCHES (#36190)
add
2025-09-22 15:18:43 -07:00
commaci-public
cd33562379 [bot] Update Python packages (#36188)
Update Python packages

Co-authored-by: Vehicle Researcher <user@comma.ai>
2025-09-22 13:43:26 -07:00
Maxime Desroches
073503a6f2 fix is_dirty when fetching branch with updated (#36187)
fix is_dirty
2025-09-21 23:53:07 -07:00
Maxime Desroches
61d5a50534 Revert "fix is_dirty when switching branch with updated (#36162)"
This reverts commit 30c388aea8.
2025-09-21 22:44:14 -07:00
commaci-public
b6e0d4807a [bot] Update Python packages (#36184)
* Update Python packages

* not available anymore

* also this

* also this

* maybe?

* version

* try

* Revert "version"

This reverts commit 9ac4401b9ca59677b82736faff8baf66861df5f2.

* revert

* cffi

* issue

* comment

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Maxime Desroches <desroches.maxime@gmail.com>
2025-09-20 20:10:51 -07:00
Shane Smiskol
c7a37c06d8 Revert "Capnp memoryview (#36163)"
This reverts commit 6ed8f07cb6.

bump
2025-09-19 17:18:08 -07:00
Shane Smiskol
efbd0b9ea0 cabana typo 2025-09-19 16:47:07 -07:00
Shane Smiskol
6ed8f07cb6 Capnp memoryview (#36163)
* lock

* bump opendbc

* fix one

* and

* Add a memoryview fallback in webrtcd

* fix

* revert

* rerevert

* bump to master

---------

Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
2025-09-19 16:46:23 -07:00
mvl-boston
5164555c4f Steering Assist warning clarification (#36135)
* clarifying warning message

* more clarity with steering assist warnings
2025-09-19 16:46:05 -07:00
Jason Young
c5999702ae Honda: Add 2026 Honda Passport to release (#36179)
* bump opendbc

* regen CARS.md

* update RELEASES.md
2025-09-19 19:40:20 -04:00
Shane Smiskol
2a5de8e0f8 raylib: fix shader antialiasing (#36176)
* fix

* np
2025-09-18 17:11:53 -07:00
Shane Smiskol
d05cb31e2e raylib: fix pairing url 2025-09-18 17:03:16 -07:00
Adeeb Shihadeh
c7a9ea2bf4 add back libbz2-dev (#36172)
* add back libbz2-dev

* try this

* revert
2025-09-18 10:59:03 -07:00
Adeeb Shihadeh
b637ad49d9 vendor all fonts (#36170)
add noto color
2025-09-18 09:25:41 -07:00
Adeeb Shihadeh
c6a2c99123 prep for vendoring (#36169)
* prep for vendoring

* less stuff

* comment
2025-09-18 09:17:26 -07:00
Adeeb Shihadeh
852598fa0a fix mac build (#36168) 2025-09-18 08:34:28 -07:00
Adeeb Shihadeh
3751d9cf51 Remove libsystemd-dev from Ubuntu dependencies (#36167)
Removed 'libsystemd-dev' from the list of dependencies.
2025-09-18 08:22:15 -07:00
Maxime Desroches
30c388aea8 fix is_dirty when switching branch with updated (#36162)
* clean

* fix
2025-09-18 00:07:06 -07:00
Shane Smiskol
b622e3e0a7 raylib: generic click callback (#36166)
* not to be used outside

* same

* rm

* fix that

* another fix

* ehh probably better to still have

* optional
2025-09-17 16:33:48 -07:00
Mitchell Goff
086e33dd6e Revert "minimal ffmpeg build (#36138)"
This reverts commit 347b23055d.
2025-09-16 14:25:18 -07:00
Kacper Rączy
889ce4c4fb torqued: add DEBUG flag (#36161)
Add a debug flag to torqued
2025-09-15 21:04:33 +00:00
Jason Young
96c00271e3 pin pycapnp (#36160) 2025-09-15 14:37:52 -04:00
Adeeb Shihadeh
a6adedf6e0 prep for python pandad (#36155) 2025-09-13 11:58:49 -07:00
commaci-public
eb821ceb5c [bot] Update Python packages (#36118)
* Update Python packages

* revert tinygrad

* can cnt

* bump panda

* bump panda

* update panda test

* revert that

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-09-13 11:48:35 -07:00
Jimmy
98d61982f9 jotpluggler: add README (#36153)
* add README

* fix typo
2025-09-13 00:35:35 -07:00
Jimmy
04a26ada69 jotpluggler: fix bug with char width after scaling text (#36154)
fix bug with char width after scaling text
2025-09-13 00:35:12 -07:00
Jimmy
3e0dd06374 jotpluggler: accept --layout argument to pluggle (#36152)
accept layouts as arg to pluggle
2025-09-13 00:20:53 -07:00
Jimmy
f18828228a jotpluggler: fix layout folder path loading and total segment (#36151)
* forgot to commit this earlier with total segments

* look in correct directory
2025-09-13 00:13:27 -07:00
Jimmy
c812c3192d jotpluggler: fix hidpi/mac font scaling (#36150)
fix hidpi/mac font scaling
2025-09-12 23:53:41 -07:00
Jimmy
8d3b919ef6 jotpluggler: better defaults for zooming/fitting (#36149)
better defaults for zooming/fitting
2025-09-12 23:28:23 -07:00
Jimmy
63df46bf22 jotpluggler: store and load layouts (#36148)
* store and load layouts

* torque controller layout

* ignore missing yaml stubs for mypy
2025-09-12 23:20:12 -07:00
Jimmy
826c5e96a1 jotpluggler: migrate logs (#36147)
migrate logs
2025-09-12 22:15:56 -07:00
Jimmy
1870d4905b jotpluggler: add tabs to layout (#36146)
* queue syncs in main thread to avoid Glfw Error/segfault

* tabs
2025-09-12 21:52:01 -07:00
Adeeb Shihadeh
347b23055d minimal ffmpeg build (#36138)
* min ffmpeg

* remove avfilter

* x264

* merge x264

* simpler

* pin x264

* mac

* rm that

* lil more

* move includes to lfs

* try this

* cleanup

* larch

---------

Co-authored-by: Comma Device <device@comma.ai>
2025-09-12 18:59:15 -07:00
Maxime Desroches
cbea5f198f op.sh: more robust switch for submodules 2025-09-12 16:05:49 -07:00
Jimmy
be379e188b jotpluggler: fix off by one error (#36144)
fix off by one error sometimes causing missed items in datatree
2025-09-12 14:37:00 -07:00
Jimmy
42d9bd0516 jotpluggler: sync x axes and autofit y axis (#36143)
* sync x axes of all timeseries plots

* always autofit y-axis

* fix typing
2025-09-12 14:36:50 -07:00
Armand du Parc Locmaria
3ca9f351a0 nevada model 🌵 (#36114)
cd29ffcf-01dd-4f1c-8808-dc197c174f1d
2025-09-12 12:45:52 -07:00
YassineYousfi
a1d6a062a9 add PR ref to new driving model in RELEASES.md 2025-09-12 12:44:11 -07:00
nayan
0a4a19e1f0 not needed yet 2025-08-24 18:48:10 -04:00
nayan
261ad8ee8b add raylib toggle 2025-08-24 18:41:50 -04:00
nayan
f206083e4b Revert "make raylib the default"
This reverts commit 956eb494e9.
2025-08-24 18:15:56 -04:00
Nayan
1f9534a592 Merge branch 'master' into nayan-raylib 2025-08-24 16:18:15 -04:00
nayan
ca7c4813f4 change padding 2025-08-23 23:42:49 -04:00
nayan
a595e6cc89 panel bg color 2025-08-23 23:37:31 -04:00
nayan
01087045cc add param to togglesp 2025-08-23 23:15:39 -04:00
nayan
e98738954c ruff, ruff, lint 2025-08-23 22:47:48 -04:00
nayan
9dd0fc8499 check for param 2025-08-23 21:39:34 -04:00
nayan
097b540795 use sp toggles on developer panel 2025-08-23 21:39:19 -04:00
nayan
5e7c20e705 this is better 2025-08-23 21:29:32 -04:00
nayan
956eb494e9 make raylib the default 2025-08-23 21:04:11 -04:00
nayan
6621e58303 fix display value 2025-08-23 20:59:50 -04:00
nayan
ab0ddbedeb Merge remote-tracking branch 'origin/master' into nayan-raylib 2025-08-23 20:42:29 -04:00
nayan
f5c106b741 refactor 1 2025-08-14 21:01:02 -04:00
nayan
9b237b5d44 Merge remote-tracking branch 'origin/master' into nayan-raylib
# Conflicts:
#	common/params_keys.h
#	system/manager/manager.py
#	system/manager/process_config.py
2025-08-14 19:36:17 -04:00
nayan
e9768e555e more device panel 2025-07-22 23:09:35 -04:00
nayan
e4ceaf1013 fix icons sizing 2025-07-22 23:00:50 -04:00
nayan
608e42eba9 optionControl - dynamic label & fix total width, device panel updates, list_view cleanup 2025-07-22 22:49:04 -04:00
nayan
f332ea051a multibutton param & style changes 2025-07-22 15:53:47 -04:00
nayan
17c84dde2a fix init for option control 2025-07-22 12:53:25 -04:00
nayan
4a1fb82a35 lint.. LINT.!! LIIINNNTTTT...!!!! 2025-07-22 11:58:15 -04:00
nayan
686d4594d8 better, bigger toggles 2025-07-22 11:18:35 -04:00
nayan
0ba208e189 add bg for selected panel button 2025-07-22 11:17:49 -04:00
nayan
5cf0de0485 option control & split device panel 2025-07-21 22:58:32 -04:00
nayan
c995726c62 update name to sp 2025-07-21 19:45:35 -04:00
nayan
8e788cd609 fix spinner 2025-07-21 18:31:42 -04:00
nayan
164800184b ignore lint 2025-07-21 18:25:29 -04:00
nayan
9424252ee5 stock ui param & split settings 2025-07-21 18:17:25 -04:00
nayan
c512cd7737 add missing icons 2025-07-21 17:06:17 -04:00
nayan
a31af2a4c8 misc lint 2025-07-21 16:59:11 -04:00
nayan
09e392afad don't register click when scrolling 2025-07-21 16:54:39 -04:00
nayan
7b0376fdf6 fix scrolling 2025-07-21 16:50:54 -04:00
nayan
5dee9c5b67 Merge remote-tracking branch 'origin/master-new' into nayan-raylib 2025-07-21 12:47:29 -04:00
nayan
c997bf2a23 Merge remote-tracking branch 'origin/master-new' into nayan-raylib 2025-07-19 17:44:29 -04:00
nayan
9a437424f4 Merge remote-tracking branch 'origin/master-new' into nayan-raylib
# Conflicts:
#	common/params_keys.h
#	selfdrive/ui/layouts/settings/settings.py
2025-07-19 15:31:01 -04:00
nayan
bcaca1a534 Merge remote-tracking branch 'origin/master-new' into nayan-raylib 2025-07-17 18:00:58 -04:00
nayan
5871eafc05 use pyui with param 2025-07-05 15:11:10 -04:00
nayan
8d36a24218 use pyui with param 2025-07-05 13:51:03 -04:00
nayan
46f634ed63 black panels 2025-07-05 10:46:30 -04:00
nayan
e2f3b76666 option control 2025-07-04 19:05:13 -04:00
nayan
be72e3e1c8 not needded 2025-07-04 18:07:12 -04:00
nayan
1fea34f96b let there be icons.. most of them 2025-07-01 17:08:45 -04:00
nayan
c1329719a7 sp controls 2025-07-01 16:08:38 -04:00
nayan
f870d00876 better approach 2025-07-01 15:54:02 -04:00
nayan
cae4ce5668 move to styles 2025-07-01 14:00:31 -04:00
nayan
c4f8055da2 update toggle & list view 2025-07-01 13:45:32 -04:00
nayan
8f65d84d2f init sp panels & toggle 2025-06-30 19:22:15 -04:00
518 changed files with 24087 additions and 59333 deletions

View File

@@ -1,19 +0,0 @@
---
Checks: '
bugprone-*,
-bugprone-integer-division,
-bugprone-narrowing-conversions,
performance-*,
clang-analyzer-*,
misc-*,
-misc-unused-parameters,
modernize-*,
-modernize-avoid-c-arrays,
-modernize-deprecated-headers,
-modernize-use-auto,
-modernize-use-using,
-modernize-use-nullptr,
-modernize-use-trailing-return-type,
'
CheckOptions:
...

View File

@@ -13,27 +13,6 @@
*.o-*
*.os
*.os-*
*.so
*.a
venv/
.venv/
notebooks
phone
massivemap
neos
installer
chffr/app2
chffr/backend/env
selfdrive/nav
selfdrive/baseui
selfdrive/test/simulator2
**/cache_data
xx/plus
xx/community
xx/projects
!xx/projects/eon_testing_master
!xx/projects/map3d
xx/ops
xx/junk

4
.gitattributes vendored
View File

@@ -7,10 +7,12 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text
system/hardware/tici/updater_weston filter=lfs diff=lfs merge=lfs -text
system/hardware/tici/updater_magic filter=lfs diff=lfs merge=lfs -text
third_party/**/*.a filter=lfs diff=lfs merge=lfs -text
third_party/**/*.so filter=lfs diff=lfs merge=lfs -text
third_party/**/*.so.* filter=lfs diff=lfs merge=lfs -text

View File

@@ -16,7 +16,7 @@ simulation:
ui:
- changed-files:
- any-glob-to-all-files: '{selfdrive/ui/**,system/ui/**}'
- any-glob-to-all-files: '{selfdrive/assets/**,selfdrive/ui/**,system/ui/**}'
tools:
- changed-files:

View File

@@ -23,7 +23,7 @@ jobs:
- uses: ./.github/workflows/setup-with-retry
- name: Push badges
run: |
${{ env.RUN }} "scons -j$(nproc) && python3 selfdrive/ui/translations/create_badges.py"
${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py"
rm .gitattributes

View File

@@ -11,7 +11,7 @@ concurrency:
cancel-in-progress: true
jobs:
selfdrive_tests:
uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master
tests:
uses: sunnypilot/sunnypilot/.github/workflows/tests.yaml@master
with:
run_number: ${{ inputs.run_number }}

View File

@@ -1,4 +1,4 @@
name: "ui preview"
name: "raylib ui preview"
on:
push:
branches:
@@ -8,14 +8,16 @@ on:
branches:
- 'master'
paths:
- 'selfdrive/assets/**'
- 'selfdrive/ui/**'
- 'system/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create UI Report"
UI_JOB_NAME: "Create raylib UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}"
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui"
jobs:
preview:
@@ -52,7 +54,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ steps.get_run_id.outputs.run_id }}
search_artifacts: true
name: report-1-${{ env.REPORT_NAME }}
name: raylib-report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui
@@ -60,23 +62,23 @@ jobs:
with:
repository: sunnypilot/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/master_ui
ref: openpilot_master_ui
path: ${{ github.workspace }}/master_ui_raylib
ref: openpilot_master_ui_raylib
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git checkout --orphan=new_master_ui
git checkout --orphan=new_master_ui_raylib
git rm -rf *
git branch -D openpilot_master_ui
git branch -m openpilot_master_ui
git branch -D openpilot_master_ui_raylib
git branch -m openpilot_master_ui_raylib
git config user.name "GitHub Actions Bot"
git config user.email "<>"
mv ${{ github.workspace }}/pr_ui/*.png .
git add .
git commit -m "screenshots for commit ${{ env.SHA }}"
git push origin openpilot_master_ui --force
git commit -m "raylib screenshots for commit ${{ env.SHA }}"
git push origin openpilot_master_ui_raylib --force
- name: Finding diff
if: github.event_name == 'pull_request_target'
@@ -94,7 +96,7 @@ jobs:
for ((i=0; i<${#A[*]}; i=i+1));
do
# Check if the master file exists
if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then
if [ ! -f "${{ github.workspace }}/master_ui_raylib/${A[$i]}.png" ]; then
# This is a new file in PR UI that doesn't exist in master
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
@@ -106,12 +108,12 @@ jobs:
DIFF="${DIFF}</table>"
DIFF="${DIFF}</details>"
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png
composite mask.png ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png
convert -delay 100 ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png
convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
mv ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png
mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>"
@@ -149,7 +151,7 @@ jobs:
- name: Saving proposed ui
if: github.event_name == 'pull_request_target'
working-directory: ${{ github.workspace }}/master_ui
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git config user.name "GitHub Actions Bot"
git config user.email "<>"
@@ -157,7 +159,7 @@ jobs:
git rm -rf *
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "screenshots for PR #${{ github.event.number }}"
git commit -m "raylib screenshots for PR #${{ github.event.number }}"
git push origin ${{ env.BRANCH_NAME }} --force
- name: Comment Screenshots on PR
@@ -165,9 +167,9 @@ jobs:
uses: thollander/actions-comment-pull-request@v2
with:
message: |
<!-- _(run_id_screenshots **${{ github.run_id }}**)_ -->
## UI Preview
<!-- _(run_id_screenshots_raylib **${{ github.run_id }}**)_ -->
## raylib UI Preview
${{ steps.find_diff.outputs.DIFF }}
comment_tag: run_id_screenshots
comment_tag: run_id_screenshots_raylib
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,4 +1,4 @@
name: selfdrive
name: tests
on:
push:
@@ -14,7 +14,7 @@ on:
type: string
concurrency:
group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
@@ -195,8 +195,6 @@ jobs:
# Pre-compile Python bytecode so each pytest worker doesn't need to
$PYTEST --collect-only -m 'not slow' -qq && \
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
chmod -R 777 /tmp/comma_download_cache"
process_replay:
@@ -257,7 +255,7 @@ jobs:
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
if: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
if: false # FIXME: Started to timeout recently
steps:
- uses: actions/checkout@v4
with:
@@ -274,38 +272,28 @@ jobs:
source selfdrive/test/setup_vsound.sh && \
CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py"
create_ui_report:
# This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml
name: Create UI Report
create_raylib_ui_report:
name: Create raylib UI Report
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
if: false # FIXME: FrameReader is broken on CI runners
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: caching frames
id: frames-cache
uses: actions/cache@v4
with:
path: .ci_cache/comma_download_cache
key: ui_screenshots_test_${{ hashFiles('selfdrive/ui/tests/test_ui/run.py') }}
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Create Test Report
timeout-minutes: ${{ ((steps.frames-cache.outputs.cache-hit == 'true') && 2 || 4) }}
- name: Create raylib UI Report
run: >
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
CACHE_ROOT=/tmp/comma_download_cache python3 selfdrive/ui/tests/test_ui/run.py &&
chmod -R 777 /tmp/comma_download_cache"
- name: Upload Test Report
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py"
- name: Upload Raylib UI Report
uses: actions/upload-artifact@v4
with:
name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/test_ui/report_1/screenshots
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots

12
.gitignore vendored
View File

@@ -10,7 +10,6 @@ venv/
.overlay_init
.overlay_consistent
.sconsign.dblite
model2.png
a.out
.hypothesis
.cache/
@@ -37,29 +36,23 @@ a.out
*.class
*.pyxbldc
*.vcd
*.qm
*.mo
*_pyx.cpp
*.stats
config.json
clcache
compile_commands.json
compare_runtime*.html
persist
selfdrive/pandad/pandad
cereal/services.h
cereal/gen
cereal/messaging/bridge
selfdrive/mapd/default_speeds_by_region.json
selfdrive/ui/translations/tmp
selfdrive/test/longitudinal_maneuvers/out
selfdrive/car/tests/cars_dump
system/camerad/camerad
system/camerad/test/ae_gray_test
notebooks
hyperthneed
provisioning
.coverage*
coverage.xml
htmlcov
@@ -76,6 +69,7 @@ sunnypilot/modeld*/thneed/compile
sunnypilot/modeld*/models/*.thneed
sunnypilot/modeld*/models/*.pkl
# openpilot log files
*.bz2
*.zst

2
.gitmodules vendored
View File

@@ -15,7 +15,7 @@
url = https://github.com/commaai/teleoprtc
[submodule "tinygrad"]
path = tinygrad_repo
url = https://github.com/tinygrad/tinygrad.git
url = https://github.com/commaai/tinygrad.git
[submodule "sunnypilot/neural_network_data"]
path = sunnypilot/neural_network_data
url = https://github.com/sunnypilot/neural-network-data.git

View File

@@ -9,4 +9,6 @@ WORKDIR ${OPENPILOT_PATH}
COPY . ${OPENPILOT_PATH}/
RUN scons --cache-readonly -j$(nproc)
ENV UV_BIN="/home/batman/.local/bin/"
ENV PATH="$UV_BIN:$PATH"
RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc)

6
Jenkinsfile vendored
View File

@@ -167,7 +167,7 @@ node {
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
'release-tici', 'testing-closet*', 'hotfix-*']
'release-tici', 'release-tizi', 'release-tizi-staging', 'testing-closet*', 'hotfix-*']
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {
@@ -178,8 +178,8 @@ node {
try {
if (env.BRANCH_NAME == 'devel-staging') {
deviceStage("build release3-staging", "tizi-needs-can", [], [
step("build release3-staging", "RELEASE_BRANCH=release3-staging $SOURCE_DIR/release/build_release.sh"),
deviceStage("build release-tizi-staging", "tizi-needs-can", [], [
step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging $SOURCE_DIR/release/build_release.sh"),
])
}

View File

@@ -1,13 +1,20 @@
Version 0.10.2 (2025-11-23)
========================
Version 0.10.1 (2025-09-08)
========================
* New driving model
* New driving model #36276
* World Model: removed global localization inputs
* World Model: 2x the number of parameters
* World Model: trained on 4x the number of segments
* VAE Compression Model: new architecture and training objective
* Driving Vision Model: trained on 4x the number of segments
* New Driver Monitoring model #36198
* Acura TLX 2021 support thanks to MVL!
* Honda City 2023 support thanks to vanillagorillaa and drFritz!
* Honda N-Box 2018 support thanks to miettal!
* Honda Odyssey 2021-25 support thanks to csouers and MVL!
* Honda Passport 2026 support thanks to vanillagorillaa and MVL!
Version 0.10.0 (2025-08-05)
========================

View File

@@ -3,176 +3,52 @@ import subprocess
import sys
import sysconfig
import platform
import shlex
import numpy as np
import SCons.Errors
SCons.Warnings.warningAsException(True)
# pending upstream fix - https://github.com/SCons/scons/issues/4461
#SetOption('warn', 'all')
TICI = os.path.isfile('/TICI')
AGNOS = TICI
Decider('MD5-timestamp')
SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
AddOption('--kaitai',
action='store_true',
help='Regenerate kaitai struct parsers')
AddOption('--asan',
action='store_true',
help='turn on ASAN')
AddOption('--ubsan',
action='store_true',
help='turn on UBSan')
AddOption('--coverage',
action='store_true',
help='build with test coverage options')
AddOption('--clazy',
action='store_true',
help='build with clazy')
AddOption('--ccflags',
action='store',
type='string',
default='',
help='pass arbitrary flags over the command line')
AddOption('--external-sconscript',
action='store',
metavar='FILE',
dest='external_sconscript',
help='add an external SConscript to the build')
AddOption('--mutation',
action='store_true',
help='generate mutation-ready code')
AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers')
AddOption('--asan', action='store_true', help='turn on ASAN')
AddOption('--ubsan', action='store_true', help='turn on UBSan')
AddOption('--mutation', action='store_true', help='generate mutation-ready code')
AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line')
AddOption('--minimal',
action='store_false',
dest='extras',
default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS)
default=os.path.exists(File('#.gitattributes').abspath), # minimal by default on release branch (where there's no LFS)
help='the minimum build to run openpilot. no tests, tools, etc.')
AddOption('--stock-ui',
action='store_true',
dest='stock_ui',
default=False,
help='Build stock openpilot UI instead of sunnypilot UI')
## Architecture name breakdown (arch)
## - larch64: linux tici aarch64
## - aarch64: linux pc aarch64
## - x86_64: linux pc x64
## - Darwin: mac x64 or arm64
real_arch = arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
# Detect platform
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
if platform.system() == "Darwin":
arch = "Darwin"
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
elif arch == "aarch64" and AGNOS:
elif arch == "aarch64" and os.path.isfile('/TICI'):
arch = "larch64"
assert arch in ["larch64", "aarch64", "x86_64", "Darwin"]
lenv = {
"PATH": os.environ['PATH'],
"PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath,
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
}
rpath = []
if arch == "larch64":
cpppath = [
"#third_party/opencl/include",
]
libpath = [
"/usr/local/lib",
"/system/vendor/lib64",
f"#third_party/acados/{arch}/lib",
]
libpath += [
"#third_party/snpe/larch64",
"#third_party/libyuv/larch64/lib",
"/usr/lib/aarch64-linux-gnu"
]
cflags = ["-DQCOM2", "-mcpu=cortex-a57"]
cxxflags = ["-DQCOM2", "-mcpu=cortex-a57"]
rpath += ["/usr/local/lib"]
else:
cflags = []
cxxflags = []
cpppath = []
rpath += []
# MacOS
if arch == "Darwin":
libpath = [
f"#third_party/libyuv/{arch}/lib",
f"#third_party/acados/{arch}/lib",
f"{brew_prefix}/lib",
f"{brew_prefix}/opt/openssl@3.0/lib",
"/System/Library/Frameworks/OpenGL.framework/Libraries",
]
cflags += ["-DGL_SILENCE_DEPRECATION"]
cxxflags += ["-DGL_SILENCE_DEPRECATION"]
cpppath += [
f"{brew_prefix}/include",
f"{brew_prefix}/opt/openssl@3.0/include",
]
# Linux
else:
libpath = [
f"#third_party/acados/{arch}/lib",
f"#third_party/libyuv/{arch}/lib",
"/usr/lib",
"/usr/local/lib",
]
if arch == "x86_64":
libpath += [
f"#third_party/snpe/{arch}"
]
rpath += [
Dir(f"#third_party/snpe/{arch}").abspath,
]
if GetOption('asan'):
ccflags = ["-fsanitize=address", "-fno-omit-frame-pointer"]
ldflags = ["-fsanitize=address"]
elif GetOption('ubsan'):
ccflags = ["-fsanitize=undefined"]
ldflags = ["-fsanitize=undefined"]
else:
ccflags = []
ldflags = []
# no --as-needed on mac linker
if arch != "Darwin":
ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"]
if not GetOption('stock_ui'):
cflags += ["-DSUNNYPILOT"]
cxxflags += ["-DSUNNYPILOT"]
ccflags_option = GetOption('ccflags')
if ccflags_option:
ccflags += ccflags_option.split(' ')
assert arch in [
"larch64", # linux tici arm64
"aarch64", # linux pc arm64
"x86_64", # linux pc x64
"Darwin", # macOS arm64 (x86 not supported)
]
env = Environment(
ENV=lenv,
ENV={
"PATH": os.environ['PATH'],
"PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath,
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
},
CC='clang',
CXX='clang++',
CCFLAGS=[
"-g",
"-fPIC",
@@ -185,37 +61,32 @@ env = Environment(
"-Wno-c99-designator",
"-Wno-reorder-init-list",
"-Wno-vla-cxx-extension",
] + cflags + ccflags,
CPPPATH=cpppath + [
],
CFLAGS=["-std=gnu11"],
CXXFLAGS=["-std=c++1z"],
CPPPATH=[
"#",
"#msgq",
"#third_party",
"#third_party/json11",
"#third_party/linux/include",
"#third_party/acados/include",
"#third_party/acados/include/blasfeo/include",
"#third_party/acados/include/hpipm/include",
"#third_party/catch2/include",
"#third_party/libyuv/include",
"#third_party/json11",
"#third_party/linux/include",
"#third_party/snpe/include",
"#third_party",
"#msgq",
],
CC='clang',
CXX='clang++',
LINKFLAGS=ldflags,
RPATH=rpath,
CFLAGS=["-std=gnu11"] + cflags,
CXXFLAGS=["-std=c++1z"] + cxxflags,
LIBPATH=libpath + [
LIBPATH=[
"#common",
"#msgq_repo",
"#third_party",
"#selfdrive/pandad",
"#common",
"#rednose/helpers",
f"#third_party/libyuv/{arch}/lib",
f"#third_party/acados/{arch}/lib",
],
RPATH=[],
CYTHONCFILESUFFIX=".cpp",
COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#",
@@ -223,30 +94,72 @@ env = Environment(
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
)
if arch == "Darwin":
# RPATH is not supported on macOS, instead use the linker flags
darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]]
env["LINKFLAGS"] += darwin_rpath_link_flags
# Arch-specific flags and paths
if arch == "larch64":
env.Append(CPPPATH=["#third_party/opencl/include"])
env.Append(LIBPATH=[
"/usr/local/lib",
"/system/vendor/lib64",
"/usr/lib/aarch64-linux-gnu",
"#third_party/snpe/larch64",
])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
env.Append(CCFLAGS=arch_flags)
env.Append(CXXFLAGS=arch_flags)
elif arch == "Darwin":
env.Append(LIBPATH=[
f"{brew_prefix}/lib",
f"{brew_prefix}/opt/openssl@3.0/lib",
f"{brew_prefix}/opt/llvm/lib/c++",
"/System/Library/Frameworks/OpenGL.framework/Libraries",
])
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CPPPATH=[
f"{brew_prefix}/include",
f"{brew_prefix}/opt/openssl@3.0/include",
])
else:
env.Append(LIBPATH=[
"/usr/lib",
"/usr/local/lib",
])
env.CompilationDatabase('compile_commands.json')
if arch == "x86_64":
env.Append(LIBPATH=[
f"#third_party/snpe/{arch}"
])
env.Append(RPATH=[
Dir(f"#third_party/snpe/{arch}").abspath,
])
# Setup cache dir
default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache'
cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir)
CacheDir(cache_dir)
Clean(["."], cache_dir)
# Sanitizers and extra CCFLAGS from CLI
if GetOption('asan'):
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
env.Append(LINKFLAGS=["-fsanitize=address"])
elif GetOption('ubsan'):
env.Append(CCFLAGS=["-fsanitize=undefined"])
env.Append(LINKFLAGS=["-fsanitize=undefined"])
_extra_cc = shlex.split(GetOption('ccflags') or '')
if _extra_cc:
env.Append(CCFLAGS=_extra_cc)
# no --as-needed on mac linker
if arch != "Darwin":
env.Append(LINKFLAGS=["-Wl,--as-needed", "-Wl,--no-undefined"])
# progress output
node_interval = 5
node_count = 0
def progress_function(node):
global node_count
node_count += node_interval
sys.stderr.write("progress: %d\n" % node_count)
if os.environ.get('SCONS_PROGRESS'):
Progress(progress_function, interval=node_interval)
# Cython build environment
# ********** Cython build environment **********
py_include = sysconfig.get_paths()['include']
envCython = env.Clone()
envCython["CPPPATH"] += [py_include, np.get_include()]
@@ -255,84 +168,27 @@ envCython["CCFLAGS"].remove("-Werror")
envCython["LIBS"] = []
if arch == "Darwin":
envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"] + darwin_rpath_link_flags
envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"]
else:
envCython["LINKFLAGS"] = ["-pthread", "-shared"]
np_version = SCons.Script.Value(np.__version__)
Export('envCython', 'np_version')
# Qt build environment
qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"]
Export('env', 'arch')
qt_libs = []
if arch == "Darwin":
qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5"
qt_dirs = [
os.path.join(qt_env['QTDIR'], "include"),
]
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"]
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
else:
qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip()
# Setup cache dir
cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
CacheDir(cache_dir)
Clean(["."], cache_dir)
qt_env['QTDIR'] = qt_install_prefix
qt_dirs = [
f"{qt_install_headers}",
]
qt_gui_path = os.path.join(qt_install_headers, "QtGui")
qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))]
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
qt_libs = [f"Qt5{m}" for m in qt_modules]
if arch == "larch64":
qt_libs += ["GLESv2", "wayland-client"]
qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath)
elif arch != "Darwin":
qt_libs += ["GL"]
qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3')
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
qt_flags = [
"-D_REENTRANT",
"-DQT_NO_DEBUG",
"-DQT_WIDGETS_LIB",
"-DQT_GUI_LIB",
"-DQT_CORE_LIB",
"-DQT_MESSAGELOGCONTEXT",
]
qt_env['CXXFLAGS'] += qt_flags
qt_env['LIBPATH'] += ['#selfdrive/ui', ]
qt_env['LIBS'] = qt_libs
if GetOption("clazy"):
checks = [
"level0",
"level1",
"no-range-loop",
"no-non-pod-global-static",
]
qt_env['CXX'] = 'clazy'
qt_env['ENV']['CLAZY_IGNORE_DIRS'] = qt_dirs[0]
qt_env['ENV']['CLAZY_CHECKS'] = ','.join(checks)
Export('env', 'qt_env', 'arch', 'real_arch')
# ********** start building stuff **********
# Build common module
SConscript(['common/SConscript'])
Import('_common', '_gpucommon')
Import('_common')
common = [_common, 'json11', 'zmq']
gpucommon = [_gpucommon]
Export('common', 'gpucommon')
Export('common')
# Build messaging (cereal + msgq + socketmaster + their dependencies)
# Enable swaglog include in submodules
@@ -375,6 +231,5 @@ if Dir('#tools/cabana/').exists() and GetOption('extras'):
if arch != "larch64":
SConscript(['tools/cabana/SConscript'])
external_sconscript = GetOption('external_sconscript')
if external_sconscript:
SConscript([external_sconscript])
env.CompilationDatabase('compile_commands.json')

View File

@@ -918,6 +918,8 @@ struct ControlsState @0x97ff69c53601abf1 {
saturated @7 :Bool;
actualLateralAccel @9 :Float32;
desiredLateralAccel @10 :Float32;
desiredLateralJerk @11 :Float32;
version @12 :Int32;
}
struct LateralLQRState {
@@ -2146,13 +2148,10 @@ struct Joystick {
struct DriverStateV2 {
frameId @0 :UInt32;
modelExecutionTime @1 :Float32;
dspExecutionTimeDEPRECATED @2 :Float32;
gpuExecutionTime @8 :Float32;
rawPredictions @3 :Data;
poorVisionProb @4 :Float32;
wheelOnRightProb @5 :Float32;
leftDriverData @6 :DriverData;
rightDriverData @7 :DriverData;
@@ -2167,10 +2166,13 @@ struct DriverStateV2 {
leftBlinkProb @7 :Float32;
rightBlinkProb @8 :Float32;
sunglassesProb @9 :Float32;
occludedProb @10 :Float32;
readyProb @11 :List(Float32);
notReadyProb @12 :List(Float32);
occludedProbDEPRECATED @10 :Float32;
readyProbDEPRECATED @11 :List(Float32);
}
dspExecutionTimeDEPRECATED @2 :Float32;
poorVisionProbDEPRECATED @4 :Float32;
}
struct DriverStateDEPRECATED @0xb83c6cc593ed0a00 {
@@ -2222,6 +2224,7 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
hiStdCount @14 :UInt32;
isActiveMode @16 :Bool;
isRHD @4 :Bool;
uncertainCount @19 :UInt32;
isPreviewDEPRECATED @15 :Bool;
rhdCheckedDEPRECATED @5 :Bool;

View File

@@ -4,18 +4,12 @@ common_libs = [
'params.cc',
'swaglog.cc',
'util.cc',
'watchdog.cc',
'ratekeeper.cc'
]
_common = env.Library('common', common_libs, LIBS="json11")
files = [
'ratekeeper.cc',
'clutil.cc',
]
_gpucommon = env.Library('gpucommon', files)
Export('_common', '_gpucommon')
_common = env.Library('common', common_libs, LIBS="json11")
Export('_common')
if GetOption('extras'):
env.Program('tests/test_common',

View File

@@ -14,9 +14,13 @@ class Api:
def post(self, *args, **kwargs):
return self.service.post(*args, **kwargs)
def get_token(self, expiry_hours=1):
return self.service.get_token(expiry_hours)
def get_token(self, payload_extra=None, expiry_hours=1):
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 get_key_pair():
return CommaConnectApi(None).get_key_pair()

View File

@@ -1,18 +1,22 @@
import jwt
import os
import requests
import unicodedata
from datetime import datetime, timedelta, UTC
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version
# name : jwt signature algorithm
KEYS = {"id_rsa" : "RS256",
"id_ecdsa" : "ES256"}
class BaseApi:
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
self.dongle_id = dongle_id
self.api_host = api_host
self.user_agent = user_agent
with open(f'{Paths.persist_root()}/comma/id_rsa') as f:
self.private_key = f.read()
self.jwt_algorithm, self.private_key, _ = self.get_key_pair()
def get(self, *args, **kwargs):
return self.request('GET', *args, **kwargs)
@@ -23,7 +27,7 @@ class BaseApi:
def request(self, method, endpoint, timeout=None, access_token=None, **params):
return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
def _get_token(self, expiry_hours=1, **extra_payload):
def _get_token(self, payload_extra=None, expiry_hours=1, **extra_payload):
now = datetime.now(UTC).replace(tzinfo=None)
payload = {
'identity': self.dongle_id,
@@ -32,13 +36,15 @@ class BaseApi:
'exp': now + timedelta(hours=expiry_hours),
**extra_payload
}
token = jwt.encode(payload, self.private_key, algorithm='RS256')
if payload_extra is not None:
payload.update(payload_extra)
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
if isinstance(token, bytes):
token = token.decode('utf8')
return token
def get_token(self, expiry_hours=1):
return self._get_token(expiry_hours)
def get_token(self, payload_extra=None, expiry_hours=1):
return self._get_token(payload_extra, expiry_hours)
def remove_non_ascii_chars(self, text):
normalized_text = unicodedata.normalize('NFD', text)
@@ -54,3 +60,11 @@ class BaseApi:
headers['User-Agent'] = self.user_agent + version
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
@staticmethod
def get_key_pair():
for key in KEYS:
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public:
return KEYS[key], private.read(), public.read()
return None, None, None

View File

@@ -1,9 +0,0 @@
# remove all keys that end in DEPRECATED
def strip_deprecated_keys(d):
for k in list(d.keys()):
if isinstance(k, str):
if k.endswith('DEPRECATED'):
d.pop(k)
elif isinstance(d[k], dict):
strip_deprecated_keys(d[k])
return d

View File

@@ -1,6 +1,6 @@
from functools import cache
import subprocess
from openpilot.common.run import run_cmd, run_cmd_default
from openpilot.common.utils import run_cmd, run_cmd_default
@cache

View File

@@ -1 +1 @@
#define DEFAULT_MODEL "Firehose (Default)"
#define DEFAULT_MODEL "The Cool People (Default)"

View File

@@ -66,7 +66,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LanguageSetting", {PERSISTENT | BACKUP, STRING, "main_en"}},
{"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}},
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
@@ -97,6 +97,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_DriverMonitoringUncertain", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}},
{"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
@@ -108,6 +109,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"RecordFront", {PERSISTENT | BACKUP, BOOL}},
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}},
{"ShowDebugInfo", {PERSISTENT, BOOL}},
{"RouteCount", {PERSISTENT, INT, "0"}},
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"SshEnabled", {PERSISTENT | BACKUP, BOOL}},
@@ -172,6 +174,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"sunnypilot_ui", {PERSISTENT | BACKUP, BOOL, "1"}},
{"UseRaylib", {PERSISTENT | BACKUP, BOOL, "0"}},
// MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},

View File

@@ -2,11 +2,10 @@ import numpy as np
from numbers import Number
class PIDController:
def __init__(self, k_p, k_i, k_f=0., k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
self._k_p = k_p
self._k_i = k_i
self._k_d = k_d
self.k_f = k_f # feedforward gain
if isinstance(self._k_p, Number):
self._k_p = [[0], [self._k_p]]
if isinstance(self._k_i, Number):
@@ -16,7 +15,7 @@ class PIDController:
self.set_limits(pos_limit, neg_limit)
self.i_rate = 1.0 / rate
self.i_dt = 1.0 / rate
self.speed = 0.0
self.reset()
@@ -46,12 +45,12 @@ class PIDController:
def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False):
self.speed = speed
self.p = float(error) * self.k_p
self.f = feedforward * self.k_f
self.d = error_rate * self.k_d
self.p = self.k_p * float(error)
self.d = self.k_d * error_rate
self.f = feedforward
if not freeze_integrator:
i = self.i + error * self.k_i * self.i_rate
i = self.i + self.k_i * self.i_dt * error
# Don't allow windup if already clipping
test_control = self.p + i + self.d + self.f

View File

@@ -1,30 +0,0 @@
import time
import functools
from openpilot.common.swaglog import cloudlog
def retry(attempts=3, delay=1.0, ignore_failure=False):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(attempts):
try:
return func(*args, **kwargs)
except Exception:
cloudlog.exception(f"{func.__name__} failed, trying again")
time.sleep(delay)
if ignore_failure:
cloudlog.error(f"{func.__name__} failed after retry")
else:
raise Exception(f"{func.__name__} failed after retry")
return wrapper
return decorator
if __name__ == "__main__":
@retry(attempts=10)
def abc():
raise ValueError("abc failed :(")
abc()

View File

@@ -1,28 +0,0 @@
import subprocess
from contextlib import contextmanager
from subprocess import Popen, PIPE, TimeoutExpired
def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip()
def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str:
try:
return run_cmd(cmd, cwd=cwd, env=env)
except subprocess.CalledProcessError:
return default
@contextmanager
def managed_proc(cmd: list[str], env: dict[str, str]):
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
try:
yield proc
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except TimeoutExpired:
proc.kill()

View File

@@ -1,7 +1,7 @@
import os
from uuid import uuid4
from openpilot.common.file_helpers import atomic_write_in_dir
from openpilot.common.utils import atomic_write_in_dir
class TestFileHelpers:

View File

@@ -2,9 +2,14 @@ import io
import os
import tempfile
import contextlib
import subprocess
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
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
class CallbackReader:
@@ -27,7 +32,7 @@ class CallbackReader:
@contextlib.contextmanager
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str = None, newline: str = None,
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
overwrite: bool = False):
"""Write to a file atomically using a temporary file in the same directory as the destination file."""
dir_name = os.path.dirname(path)
@@ -56,3 +61,58 @@ def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.Buffered
compressed_size = compressed_stream.tell()
compressed_stream.seek(0)
return compressed_stream, compressed_size
# remove all keys that end in DEPRECATED
def strip_deprecated_keys(d):
for k in list(d.keys()):
if isinstance(k, str):
if k.endswith('DEPRECATED'):
d.pop(k)
elif isinstance(d[k], dict):
strip_deprecated_keys(d[k])
return d
def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip()
def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str:
try:
return run_cmd(cmd, cwd=cwd, env=env)
except subprocess.CalledProcessError:
return default
@contextlib.contextmanager
def managed_proc(cmd: list[str], env: dict[str, str]):
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
try:
yield proc
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except TimeoutExpired:
proc.kill()
def retry(attempts=3, delay=1.0, ignore_failure=False):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(attempts):
try:
return func(*args, **kwargs)
except Exception:
cloudlog.exception(f"{func.__name__} failed, trying again")
time.sleep(delay)
if ignore_failure:
cloudlog.error(f"{func.__name__} failed after retry")
else:
raise Exception(f"{func.__name__} failed after retry")
return wrapper
return decorator

View File

@@ -1 +1 @@
#define COMMA_VERSION "0.10.1"
#define COMMA_VERSION "0.10.2"

View File

@@ -1,12 +0,0 @@
#include <string>
#include "common/watchdog.h"
#include "common/util.h"
#include "system/hardware/hw.h"
const std::string watchdog_fn_prefix = Path::shm_path() + "/wd_"; // + <pid>
bool watchdog_kick(uint64_t ts) {
static std::string fn = watchdog_fn_prefix + std::to_string(getpid());
return util::write_file(fn.c_str(), &ts, sizeof(ts), O_WRONLY | O_CREAT) > 0;
}

View File

@@ -1,5 +0,0 @@
#pragma once
#include <cstdint>
bool watchdog_kick(uint64_t ts);

View File

@@ -1,22 +0,0 @@
import os
import time
import struct
from openpilot.system.hardware.hw import Paths
WATCHDOG_FN = f"{Paths.shm_path()}/wd_"
_LAST_KICK = 0.0
def kick_watchdog():
global _LAST_KICK
current_time = time.monotonic()
if current_time - _LAST_KICK < 1.0:
return
try:
with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f:
f.write(struct.pack('<Q', int(current_time * 1e9)))
f.flush()
_LAST_KICK = current_time
except OSError:
pass

View File

@@ -0,0 +1,65 @@
# CarState signals
## Required for basic lateral control
* `brakePressed`
* `cruiseState`
* `doorOpen`
* `espDisabled`
* `gasPressed`
* `gearShifter`
* `leftBlinker` / `rightBlinker`
* `seatbeltUnlatched`
* `standstill`
* `steeringAngleDeg`
* `steeringPressed`
* `steeringTorque`
* `steerFaultPermanent`
* `steerFaultTemporary`
* `vCruise`
* `wheelSpeeds.[fl|fr|rl|rr]`: Speed of each of the car's four wheels, in m/s. The car's CAN bus often broadcasts the
speed in kph, so the helper function `parse_wheel_speeds` performs this conversion by default.
## Recommended / Required for openpilot longitudinal control
* `accFaulted`
* `espActive`
* `parkingBrake`
## Application Dependent
* `blockPcmEnable`
* `buttonEnable`
* `brakeHoldActive`
* `carFaultedNonCritical`
* `invalidLkasSetting`
* `lowSpeedAlert`
* `regenBraking`
* `steeringAngleOffsetDeg`
* `steeringDisengage`
* `steeringTorqueEps`
* `stockLkas`
* `vCruiseCluster`
* `vEgoCluster`
* `vehicleSensorsInvalid`
## Automatically populated
* `buttonEvents`
These values are populated automatically by `parse_wheel_speeds`:
* `aEgo`: Acceleration of the ego vehicle, Kalman filtered derivative of `vEgo`.
* `vEgo`: Speed of the ego vehicle, Kalman filtered from `vEgoRaw`.
* `vEgoRaw`: Speed of the ego vehicle, based on the average of all four wheel speeds, unfiltered.
## Optional
* `brake`
* `charging`
* `fuelGauge`
* `leftBlindspot` / `rightBlindspot`
* `steeringRateDeg`
* `stockAeb`
* `stockFcw`
* `yawRate`

View File

@@ -0,0 +1,85 @@
# Stimulus-Response Tests
These are example test drives that can help identify the CAN bus messaging necessary for ADAS control. Each scripted
test should be done in a separate route (ignition cycle). These tests are a guide, not necessarily exhaustive.
While testing, constant power to the comma device is highly recommended, using [comma power](https://comma.ai/shop/comma-power) if
necessary to make sure all test activity is fully captured and for ease of uploading. If constant power isn't
available, keep the ignition on for at least one minute after your test to make sure power loss doesn't result
in loss of the last minute of testing data.
## Stationary ignition-only tests, part 1
1. Ignition on, but don't start engine, remain in Park
2. Open and close each door in a defined order: driver, passenger, rear left, rear right
3. Re-enter the vehicle, close the driver's door, and fasten the driver's seatbelt
4. Slowly press and release the accelerator pedal 3 times
5. Slowly press and release the brake pedal 3 times
6. Hold the brake and move the gearshift to reverse, then neutral, then drive, then sport/eco/etc if applicable
7. Return to Park, ignition off
Brake-pressed information may show up in several messages and signals, both as on/off states and as a percentage or
pressure. It may reflect a switch on the driver's brake pedal, or a pressure-threshold state, or signals to turn on
the rear brake lights. Start by identifying all the potential signals, and confirm while driving with ACC later.
Locate signals for all four door states if possible, but some cars only expose the driver's door state on the ADAS bus.
Driver/passenger door signals may or may not change positions for LHD vs RHD cars. For cars where only the driver's
door signal is available, the same signal may follow the driver.
## Stationary ignition-only tests, part 2
1. Ignition on, but don't start engine, remain in Park
2. Press each ACC button in a defined order: main switch on/off, set, resume, cancel, accel, decel, gap adjust
3. Set the left turn signal for about five seconds
4. Operate the left turn signal one time in its touch-to-pass mode
5. Set the right turn signal for about five seconds
6. Operate the right turn signal one time in its touch-to-pass mode
7. Set the hazard / emergency indicator switch for about five seconds
8. Ignition off
Your vehicle may have a momentary-press main ACC switch or a physical toggle that remains set. Actual ACC engagement
isn't necessary for purposes of detecting the ACC button presses.
## Steering angle and steering torque tests
Power steering should be available. On ICE cars, engine RPM may be present.
1. Ignition on, start engine if applicable, remain in Park
2. Rotate the steering wheel as follows, with a few seconds pause between each step
* Start as close to exact center as possible
* Turn to 45 degrees right and hold
* Turn to 90 degrees right and hold
* Turn to 180 degrees right and hold
* Turn to full lock right and hold, with firm pressure against lock
* Release the wheel and allow it to bounce back slightly from lock
* Turn to 180 degrees left and hold
* Return to center and release
3. Ignition off
Performing the full test to the right, followed by an abbreviated test to the left, helps give additional confirmation
of signal scale, and sign/direction for both the steering wheel angle and driver input torque signals.
## Low speed / parking lot driving tests
Before this test, drive to a place like an empty parking lot where you are free to drive in a series of curves.
1. Ignition on, start engine if applicable, prepare to drive
2. Slowly (10-20mph at most) drive a figure-8 if possible, or at least one sharp left and one sharp right.
3. Come to a complete stop
4. When and where safe, drive in reverse for a short distance (10-15 feet)
5. Park the car in a safe place, ignition off
## High speed / highway driving tests
Select a place and time where you can safely set cruise control at normal travel speeds with little interference from
traffic ahead, and safely test the response of your factory lane guidance system.
1. Ignition on, start engine if applicable, prepare to drive
2. When safely able, engage adaptive cruise control below 50 mph
3. When safely able, use the ACC buttons to accelerate to 50mph, then 55mph, then 60mph
4. Disengage adaptive cruise
5. When safely able, allow your factory lane guidance to prevent lane departures, 2-3 times on both the left and right
The series of setpoints can be adjusted to local traffic regulations, and of course metric units. The specific cruise
setpoints are useful for locating the ACC HUD signals later, and confirming their precise scaling. When the car reaches
and holds the setpoint, that can also provide additional confirmation of wheel speed scaling.

View File

@@ -6,8 +6,17 @@ export NUMEXPR_NUM_THREADS=1
export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
# models get lower priority than ui
# - ui is ~5ms
# - modeld is 20ms
# - DM is 10ms
# in order to run ui at 60fps (16.67ms), we need to allow
# it to preempt the model workloads. we have enough
# headroom for this until ui is moved to the CPU.
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="13.1"
export AGNOS_VERSION="15"
fi
export STAGING_ROOT="/data/safe_staging"

2
panda

Submodule panda updated: 69ab12ee2a...e4115086b0

View File

@@ -23,7 +23,7 @@ dependencies = [
# core
"cffi",
"scons",
"pycapnp",
"pycapnp==2.1.0",
"Cython",
"setuptools",
"numpy >=2.0",
@@ -72,7 +72,9 @@ dependencies = [
"zstandard",
# ui
"raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186
"qrcode",
"mapbox-earcut",
]
[project.optional-dependencies]
@@ -119,7 +121,6 @@ dev = [
"tabulate",
"types-requests",
"types-tabulate",
"raylib",
]
tools = [
@@ -177,7 +178,7 @@ quiet-level = 3
# if you've got a short variable name that's getting flagged, add it here
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite"
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/*, *.ts, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*"
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/*"
[tool.mypy]
python_version = "3.11"
@@ -235,7 +236,6 @@ lint.ignore = [
"B027",
"B024",
"NPY002", # new numpy random syntax is worse
"UP038", # (x, y) -> x|y for isinstance
]
line-length = 160
target-version ="py311"
@@ -263,8 +263,13 @@ lint.flake8-implicit-str-concat.allow-multiline = false
"tools".msg = "Use openpilot.tools"
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
"unittest".msg = "Use pytest"
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
"time.time".msg = "Use time.monotonic"
# raylib banned APIs
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
"pyray.is_mouse_button_pressed".msg = "This can miss events. Use Widget._handle_mouse_press"
"pyray.is_mouse_button_released".msg = "This can miss events. Use Widget._handle_mouse_release"
"pyray.draw_text".msg = "Use a function (such as rl.draw_font_ex) that takes font as an argument"
[tool.ruff.format]
quote-style = "preserve"

View File

@@ -12,7 +12,7 @@ from openpilot.common.basedir import BASEDIR
DIRS = ['cereal', 'openpilot']
EXTS = ['.png', '.py', '.ttf', '.capnp']
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo']
INTERPRETER = '/usr/bin/env python3'

View File

@@ -3,4 +3,4 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
SConscript(['locationd/SConscript'])
SConscript(['modeld/SConscript'])
SConscript(['ui/SConscript'])
SConscript(['ui/SConscript'])

View File

@@ -1,2 +1,4 @@
*.cc
fonts/*.fnt
fonts/*.png
translations_assets.qrc

Binary file not shown.

128
selfdrive/assets/fonts/process.py Executable file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
from pathlib import Path
import json
import pyray as rl
FONT_DIR = Path(__file__).resolve().parent
SELFDRIVE_DIR = FONT_DIR.parents[1]
TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations"
LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
GLYPH_PADDING = 6
EXTRA_CHARS = "–‑✓×°§•€£¥"
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
def _languages():
if not LANGUAGES_FILE.exists():
return {}
with LANGUAGES_FILE.open(encoding="utf-8") as f:
return json.load(f)
def _char_sets():
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
unifont = set(base)
for language, code in _languages().items():
unifont.update(language)
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
try:
chars = set(po_path.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
def _glyph_metrics(glyphs, rects, codepoints):
entries = []
min_offset_y, max_extent = None, 0
for idx, codepoint in enumerate(codepoints):
glyph = glyphs[idx]
rect = rects[idx]
width = int(round(rect.width))
height = int(round(rect.height))
offset_y = int(round(glyph.offsetY))
min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y)
max_extent = max(max_extent, offset_y + height)
entries.append({
"id": codepoint,
"x": int(round(rect.x)),
"y": int(round(rect.y)),
"width": width,
"height": height,
"xoffset": int(round(glyph.offsetX)),
"yoffset": offset_y,
"xadvance": int(round(glyph.advanceX)),
})
if min_offset_y is None:
raise RuntimeError("No glyphs were generated")
line_height = int(round(max_extent - min_offset_y))
base = int(round(max_extent))
return entries, line_height, base
def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries):
lines = [
f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0",
f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4",
f"page id=0 file=\"{atlas_name}\"",
f"chars count={len(entries)}",
]
for entry in entries:
lines.append(
("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " +
"xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry)
)
path.write_text("\n".join(lines) + "\n")
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
print(f"Processing {font_path.name}...")
font_size = {
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
}.get(font_path.name, 200)
data = font_path.read_bytes()
file_buf = rl.ffi.new("unsigned char[]", data)
cp_buffer = rl.ffi.new("int[]", codepoints)
cp_ptr = rl.ffi.cast("int *", cp_buffer)
glyphs = rl.load_font_data(rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints), rl.FontType.FONT_DEFAULT)
if glyphs == rl.ffi.NULL:
raise RuntimeError("raylib failed to load font data")
rects_ptr = rl.ffi.new("Rectangle **")
image = rl.gen_image_font_atlas(glyphs, rects_ptr, len(codepoints), font_size, GLYPH_PADDING, 0)
if image.width == 0 or image.height == 0:
raise RuntimeError("raylib returned an empty atlas")
rects = rects_ptr[0]
atlas_name = f"{font_path.stem}.png"
atlas_path = FONT_DIR / atlas_name
entries, line_height, base = _glyph_metrics(glyphs, rects, codepoints)
if not rl.export_image(image, atlas_path.as_posix()):
raise RuntimeError("Failed to export atlas image")
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
def main():
base_cp, unifont_cp = _char_sets()
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
for font in fonts:
if "emoji" in font.name.lower():
continue
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
_process_font(font, glyphs)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

View File

@@ -62,8 +62,8 @@ class TestCarInterfaces:
# hypothesis also slows down significantly with just one more message draw
LongControl(car_params, car_params_sp)
if car_params.steerControlType == CarParams.SteerControlType.angle:
LatControlAngle(car_params, car_params_sp, car_interface)
LatControlAngle(car_params, car_params_sp, car_interface, DT_CTRL)
elif car_params.lateralTuning.which() == 'pid':
LatControlPID(car_params, car_params_sp, car_interface)
LatControlPID(car_params, car_params_sp, car_interface, DT_CTRL)
elif car_params.lateralTuning.which() == 'torque':
LatControlTorque(car_params, car_params_sp, car_interface)
LatControlTorque(car_params, car_params_sp, car_interface, DT_CTRL)

View File

@@ -189,7 +189,7 @@ class TestCarModelBase(unittest.TestCase):
if tuning == 'pid':
self.assertTrue(len(self.CP.lateralTuning.pid.kpV))
elif tuning == 'torque':
self.assertTrue(self.CP.lateralTuning.torque.kf > 0)
self.assertTrue(self.CP.lateralTuning.torque.latAccelFactor > 0)
else:
raise Exception("unknown tuning")

View File

@@ -8,7 +8,7 @@ from cereal import car, log
import cereal.messaging as messaging
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority, Ratekeeper
from openpilot.common.swaglog import cloudlog
from opendbc.car.car_helpers import interfaces
@@ -19,6 +19,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
@@ -45,7 +46,7 @@ class Controls(ControlsExt, ModelStateBase):
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
self.sm = messaging.SubMaster(['liveDelay', 'liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
poll='selfdriveState')
@@ -62,11 +63,11 @@ class Controls(ControlsExt, ModelStateBase):
self.VM = VehicleModel(self.CP)
self.LaC: LatControl
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI)
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI, DT_CTRL)
elif self.CP.lateralTuning.which() == 'pid':
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI)
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI, DT_CTRL)
elif self.CP.lateralTuning.which() == 'torque':
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI)
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI, DT_CTRL)
def update(self):
self.sm.update(15)
@@ -139,11 +140,12 @@ class Controls(ControlsExt, ModelStateBase):
# Reset desired curvature to current to avoid violating the limits on engage
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)
lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
actuators.curvature = self.desired_curvature
steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp,
self.steer_limited_by_safety, self.desired_curvature,
self.calibrated_pose, curvature_limited) # TODO what if not available
self.calibrated_pose, curvature_limited, lat_delay)
actuators.torque = float(steer)
actuators.steeringAngleDeg = float(steeringAngleDeg)
# Ensure no NaNs/Infs

View File

@@ -22,7 +22,7 @@ def smooth_value(val, prev_val, tau, dt=DT_MDL):
alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1
return alpha * val + (1 - alpha) * prev_val
def clip_curvature(v_ego, prev_curvature, new_curvature, roll):
def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, bool]:
# This function respects ISO lateral jerk and acceleration limits + a max curvature
v_ego = max(v_ego, MIN_SPEED)
max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755

View File

@@ -1,31 +1,31 @@
import numpy as np
from abc import abstractmethod, ABC
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.helpers import Pose
class LatControl(ABC):
def __init__(self, CP, CP_SP, CI):
self.sat_count_rate = 1.0 * DT_CTRL
def __init__(self, CP, CP_SP, CI, dt):
self.dt = dt
self.sat_limit = CP.steerLimitTimer
self.sat_count = 0.
self.sat_time = 0.
self.sat_check_min_speed = 10.
# we define the steer torque scale as [-1.0...1.0]
self.steer_max = 1.0
@abstractmethod
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose,
curvature_limited: bool, lat_delay: float):
pass
def reset(self):
self.sat_count = 0.
self.sat_time = 0.
def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited):
# Saturated only if control output is not being limited by car torque/angle rate limits
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed:
self.sat_count += self.sat_count_rate
self.sat_time += self.dt
else:
self.sat_count -= self.sat_count_rate
self.sat_count = np.clip(self.sat_count, 0.0, self.sat_limit)
return self.sat_count > (self.sat_limit - 1e-3)
self.sat_time -= self.dt
self.sat_time = np.clip(self.sat_time, 0.0, self.sat_limit)
return self.sat_time > (self.sat_limit - 1e-3)

View File

@@ -8,12 +8,12 @@ STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
class LatControlAngle(LatControl):
def __init__(self, CP, CP_SP, CI):
super().__init__(CP, CP_SP, CI)
def __init__(self, CP, CP_SP, CI, dt):
super().__init__(CP, CP_SP, CI, dt)
self.sat_check_min_speed = 5.
self.use_steer_limited_by_safety = CP.brand == "tesla"
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
angle_log = log.ControlsState.LateralAngleState.new_message()
if not active:

View File

@@ -6,14 +6,15 @@ from openpilot.common.pid import PIDController
class LatControlPID(LatControl):
def __init__(self, CP, CP_SP, CI):
super().__init__(CP, CP_SP, CI)
def __init__(self, CP, CP_SP, CI, dt):
super().__init__(CP, CP_SP, CI, dt)
self.pid = PIDController((CP.lateralTuning.pid.kpBP, CP.lateralTuning.pid.kpV),
(CP.lateralTuning.pid.kiBP, CP.lateralTuning.pid.kiV),
k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
pos_limit=self.steer_max, neg_limit=-self.steer_max)
self.ff_factor = CP.lateralTuning.pid.kf
self.get_steer_feedforward = CI.get_steer_feedforward_function()
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
pid_log = log.ControlsState.LateralPIDState.new_message()
pid_log.steeringAngleDeg = float(CS.steeringAngleDeg)
pid_log.steeringRateDeg = float(CS.steeringRateDeg)
@@ -30,7 +31,7 @@ class LatControlPID(LatControl):
else:
# offset does not contribute to resistive torque
ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
ff = self.ff_factor * self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_torque = self.pid.update(error,

View File

@@ -1,9 +1,11 @@
import math
import numpy as np
from collections import deque
from cereal import log
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.common.pid import PIDController
@@ -15,25 +17,34 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La
# wheel slip, or to speed.
# This controller applies torque to achieve desired lateral
# accelerations. To compensate for the low speed effects we
# use a LOW_SPEED_FACTOR in the error. Additionally, there is
# friction in the steering wheel that needs to be overcome to
# move it at all, this is compensated for too.
# accelerations. To compensate for the low speed effects the
# proportional gain is increased at low speeds by the PID controller.
# Additionally, there is friction in the steering wheel that needs
# to be overcome to move it at all, this is compensated for too.
LOW_SPEED_X = [0, 10, 20, 30]
LOW_SPEED_Y = [15, 13, 10, 5]
KP = 1.0
KI = 0.3
KD = 0.0
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
LP_FILTER_CUTOFF_HZ = 1.2
LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0
VERSION = 0
class LatControlTorque(LatControl):
def __init__(self, CP, CP_SP, CI):
super().__init__(CP, CP_SP, CI)
def __init__(self, CP, CP_SP, CI, dt):
super().__init__(CP, CP_SP, CI, dt)
self.torque_params = CP.lateralTuning.torque.as_builder()
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
k_f=self.torque_params.kf)
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt)
self.update_limits()
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt)
self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len)
self.previous_measurement = 0.0
self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
@@ -47,57 +58,68 @@ class LatControlTorque(LatControl):
self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params),
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
# Override torque params from extension
if self.extension.update_override_torque_params(self.torque_params):
self.update_limits()
pid_log = log.ControlsState.LateralTorqueState.new_message()
pid_log.version = VERSION
if not active:
output_torque = 0.0
pid_log.active = False
else:
actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
desired_lateral_accel = desired_curvature * CS.vEgo ** 2
actual_lateral_accel = actual_curvature * CS.vEgo ** 2
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
low_speed_factor = np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
measurement = actual_lateral_accel + low_speed_factor * actual_curvature
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation
delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len))
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
# TODO factor out lateral jerk from error to later replace it with delay independent alternative
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay
measurement = measured_curvature * CS.vEgo ** 2
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
self.previous_measurement = measurement
setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel
error = setpoint - measurement
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
pid_log.error = float(setpoint - measurement)
ff = gravity_adjusted_lateral_accel
pid_log.error = float(error)
ff = gravity_adjusted_future_lateral_accel
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
ff -= self.torque_params.latAccelOffset
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_lataccel = self.pid.update(pid_log.error,
feedforward=ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
-measurement_rate,
feedforward=ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
# Lateral acceleration torque controller extension updates
# Overrides pid_log.error and output_torque
pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
desired_curvature, actual_curvature, steer_limited_by_safety, output_torque)
future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel,
desired_curvature, measured_curvature, steer_limited_by_safety, output_torque)
pid_log.active = True
pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i)
pid_log.d = float(self.pid.d)
pid_log.f = float(self.pid.f)
pid_log.output = float(-output_torque) # TODO: log lat accel?
pid_log.actualLateralAccel = float(actual_lateral_accel)
pid_log.desiredLateralAccel = float(desired_lateral_accel)
pid_log.output = float(-output_torque) # TODO: log lat accel?
pid_log.actualLateralAccel = float(measurement)
pid_log.desiredLateralAccel = float(setpoint)
pid_log.desiredLateralJerk = float(desired_lateral_jerk)
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
# TODO left is positive in this convention

View File

@@ -54,7 +54,7 @@ class LongControl:
self.long_control_state = LongCtrlState.off
self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV),
(CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV),
k_f=CP.longitudinalTuning.kf, rate=1 / DT_CTRL)
rate=1 / DT_CTRL)
self.last_output_accel = 0.0
def reset(self):

View File

@@ -7,6 +7,7 @@ from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.nissan.values import CAR as NISSAN
from opendbc.car.gm.values import CAR as GM
from opendbc.car.vehicle_model import VehicleModel
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car.helpers import convert_to_capnp
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
@@ -29,7 +30,7 @@ class TestLatControl:
CP_SP = convert_to_capnp(CP_SP)
VM = VehicleModel(CP)
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI)
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
CS = car.CarState.new_message()
CS.vEgo = 30
@@ -42,13 +43,13 @@ class TestLatControl:
# Saturate for curvature limited and controller limited
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True)
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2)
assert lac_log.saturated
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False)
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2)
assert not lac_log.saturated
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False)
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2)
assert lac_log.saturated

View File

@@ -6,7 +6,7 @@ from collections import defaultdict
import matplotlib.pyplot as plt
from cereal.services import SERVICE_LIST
from openpilot.common.file_helpers import LOG_COMPRESSION_LEVEL
from openpilot.common.utils import LOG_COMPRESSION_LEVEL
from openpilot.tools.lib.logreader import LogReader
from tqdm import tqdm

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import os
import numpy as np
from collections import deque, defaultdict
@@ -250,6 +251,8 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
def main(demo=False):
config_realtime_process([0, 1, 2, 3], 5)
DEBUG = bool(int(os.getenv("DEBUG", "0")))
pm = messaging.PubMaster(['liveTorqueParameters'])
sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'liveCalibration', 'livePose', 'liveDelay'], poll='livePose')
@@ -268,7 +271,7 @@ def main(demo=False):
# 4Hz driven by livePose
if sm.frame % 5 == 0:
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks()))
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks(), with_points=DEBUG))
# Cache points every 60 seconds while onroad
if sm.frame % 240 == 0:

View File

@@ -1,11 +1,11 @@
import os
import glob
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations')
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
lenv = env.Clone()
lenvCython = envCython.Clone()
libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread']
libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread']
frameworks = []
common_src = [

View File

@@ -25,13 +25,13 @@ from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from
MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE
CALIB_LEN = 3
FEATURE_LEN = 512
OUTPUT_SIZE = 84 + FEATURE_LEN
OUTPUT_SIZE = 83 + FEATURE_LEN
PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
# TODO: slice from meta
class DriverStateResult(ctypes.Structure):
_fields_ = [
("face_orientation", ctypes.c_float*3),
@@ -46,8 +46,8 @@ class DriverStateResult(ctypes.Structure):
("left_blink_prob", ctypes.c_float),
("right_blink_prob", ctypes.c_float),
("sunglasses_prob", ctypes.c_float),
("occluded_prob", ctypes.c_float),
("ready_prob", ctypes.c_float*4),
("_unused_c", ctypes.c_float),
("_unused_d", ctypes.c_float*4),
("not_ready_prob", ctypes.c_float*2)]
@@ -55,7 +55,6 @@ class DMonitoringModelResult(ctypes.Structure):
_fields_ = [
("driver_state_lhd", DriverStateResult),
("driver_state_rhd", DriverStateResult),
("poor_vision_prob", ctypes.c_float),
("wheel_on_right_prob", ctypes.c_float),
("features", ctypes.c_float*FEATURE_LEN)]
@@ -107,8 +106,6 @@ def fill_driver_state(msg, ds_result: DriverStateResult):
msg.leftBlinkProb = float(sigmoid(ds_result.left_blink_prob))
msg.rightBlinkProb = float(sigmoid(ds_result.right_blink_prob))
msg.sunglassesProb = float(sigmoid(ds_result.sunglasses_prob))
msg.occludedProb = float(sigmoid(ds_result.occluded_prob))
msg.readyProb = [float(sigmoid(x)) for x in ds_result.ready_prob]
msg.notReadyProb = [float(sigmoid(x)) for x in ds_result.not_ready_prob]
@@ -119,7 +116,6 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts:
ds.frameId = frame_id
ds.modelExecutionTime = execution_time
ds.gpuExecutionTime = gpu_execution_time
ds.poorVisionProb = float(sigmoid(model_result.poor_vision_prob))
ds.wheelOnRightProb = float(sigmoid(model_result.wheel_on_right_prob))
ds.rawPredictions = model_output.tobytes() if SEND_RAW_PRED else b''
fill_driver_state(ds.leftDriverData, model_result.driver_state_lhd)

View File

@@ -62,6 +62,5 @@ Refer to **slice_outputs** and **parse_vision_outputs/parse_policy_outputs** in
* (deprecated) distracted probabilities: 2
* using phone probability: 1
* distracted probability: 1
* common outputs 2
* poor camera vision probability: 1
* common outputs 1
* left hand drive probability: 1

View File

@@ -1,2 +0,0 @@
fa69be01-b430-4504-9d72-7dcb058eb6dd
d9fb22d1c4fa3ca3d201dbc8edf1d0f0918e53e6

View File

@@ -4,11 +4,13 @@ import numpy as np
from cereal import car, log
import cereal.messaging as messaging
from openpilot.selfdrive.selfdrived.events import Events
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.common.realtime import DT_DMON
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.stat_live import RunningStatFilter
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.system.hardware import HARDWARE
EventName = log.OnroadEvent.EventName
@@ -34,12 +36,13 @@ class DRIVER_MONITOR_SETTINGS:
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
self._EE_THRESH11 = 0.4
if HARDWARE.get_device_type() == 'mici':
self._EE_THRESH11 = 0.75
else:
self._EE_THRESH11 = 0.4
self._EE_THRESH12 = 15.0
self._EE_MAX_OFFSET1 = 0.06
self._EE_MIN_OFFSET1 = 0.025
self._EE_THRESH21 = 0.01
self._EE_THRESH22 = 0.35
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
@@ -55,6 +58,9 @@ class DRIVER_MONITOR_SETTINGS:
self._YAW_MAX_OFFSET = 0.289
self._YAW_MIN_OFFSET = -0.0246
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / self._DT_DMON)
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / self._DT_DMON)
self._POSESTD_THRESHOLD = 0.3
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
@@ -137,11 +143,8 @@ class DriverMonitoring:
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
self.blink = DriverBlink()
self.eev1 = 0.
self.eev2 = 1.
self.ee1_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
self.ee2_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
self.ee1_calibrated = False
self.ee2_calibrated = False
self.always_on = always_on
self.distracted_types = []
@@ -159,6 +162,9 @@ class DriverMonitoring:
self.hi_stds = 0
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.dcam_uncertain_cnt = 0
self.dcam_uncertain_alerted = False # once per drive
self.dcam_reset_cnt = 0
self.params = Params()
self.too_distracted = self.params.get_bool("DriverTooDistracted")
@@ -246,7 +252,7 @@ class DriverMonitoring:
return distracted_types
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged):
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill):
rhd_pred = driver_state.wheelOnRightProb
# calibrates only when there's movement and either face detected
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
@@ -262,7 +268,7 @@ class DriverMonitoring:
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
driver_data.faceOrientationStd, driver_data.facePositionStd,
driver_data.readyProb, driver_data.notReadyProb)):
driver_data.notReadyProb)):
return
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
@@ -279,7 +285,6 @@ class DriverMonitoring:
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.eev1 = driver_data.notReadyProb[0]
self.eev2 = driver_data.readyProb[0]
self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_E2E in self.distracted_types or DistractedType.DISTRACTED_POSE in self.distracted_types
@@ -293,12 +298,20 @@ class DriverMonitoring:
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
self.ee1_offseter.push_and_update(self.eev1)
self.ee2_offseter.push_and_update(self.eev2)
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.ee1_calibrated = self.ee1_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.ee2_calibrated = self.ee2_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
if not standstill:
self.dcam_uncertain_cnt += 1
self.dcam_reset_cnt = 0
else:
self.dcam_reset_cnt += 1
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
self.dcam_uncertain_cnt = 0
self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME
self._set_timers(self.face_detected and not self.is_model_uncertain)
@@ -376,6 +389,10 @@ class DriverMonitoring:
if alert is not None:
self.current_events.add(alert)
if self.dcam_uncertain_cnt > self.settings._DCAM_UNCERTAIN_ALERT_COUNT and not self.dcam_uncertain_alerted:
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
self.dcam_uncertain_alerted = True
def get_state_packet(self, valid=True):
# build driverMonitoringState packet
@@ -397,6 +414,7 @@ class DriverMonitoring:
"hiStdCount": self.hi_stds,
"isActiveMode": self.active_monitoring_mode,
"isRHD": self.wheel_on_right,
"uncertainCount": self.dcam_uncertain_cnt,
}
return dat
@@ -412,7 +430,8 @@ class DriverMonitoring:
driver_state=sm['driverStateV2'],
cal_rpy=sm['liveCalibration'].rpyCalib,
car_speed=sm['carState'].vEgo,
op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive
op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive,
standstill=sm['carState'].standstill,
)
# Update distraction events

View File

@@ -25,7 +25,6 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
# TODO: test both separately when e2e is used
ds.leftDriverData.readyProb = [0., 0., 0., 0.]
ds.leftDriverData.notReadyProb = [0., 0.]
return ds
@@ -54,7 +53,7 @@ class TestMonitoring:
DM = DriverMonitoring()
events = []
for idx in range(len(msgs)):
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx])
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx])
# cal_rpy and car_speed don't matter here
# evaluate events at 10Hz for tests

View File

@@ -80,7 +80,7 @@ Panda *connect(std::string serial="", uint32_t index=0) {
}
//panda->enable_deepsleep();
for (int i = 0; i < PANDA_BUS_CNT; i++) {
for (int i = 0; i < PANDA_CAN_CNT; i++) {
panda->set_can_fd_auto(i, true);
}

View File

@@ -5,8 +5,7 @@ import time
import cereal.messaging as messaging
from cereal import log
from openpilot.common.gpio import gpio_set, gpio_init
from panda import Panda, PandaDFU, PandaProtocolMismatch
from openpilot.common.retry import retry
from panda import Panda, PandaDFU
from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware import HARDWARE
from openpilot.system.hardware.tici.pins import GPIO
@@ -50,8 +49,7 @@ class TestPandad:
assert not Panda.wait_for_dfu(None, 3)
assert not Panda.wait_for_panda(None, 3)
@retry(attempts=3)
def _flash_bootstub_and_test(self, fn, expect_mismatch=False):
def _flash_bootstub(self, fn):
self._go_to_dfu()
pd = PandaDFU(None)
if fn is None:
@@ -61,16 +59,6 @@ class TestPandad:
pd.reset()
HARDWARE.reset_internal_panda()
assert Panda.wait_for_panda(None, 10)
if expect_mismatch:
with pytest.raises(PandaProtocolMismatch):
Panda()
else:
with Panda() as p:
assert p.bootstub
self._run_test(45)
def test_in_dfu(self):
HARDWARE.recover_internal_panda()
self._run_test(60)
@@ -106,13 +94,14 @@ class TestPandad:
print("startup times", ts, sum(ts) / len(ts))
assert 0.1 < (sum(ts)/len(ts)) < 0.7
def test_protocol_version_check(self):
# flash old fw
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")
self._flash_bootstub_and_test(fn, expect_mismatch=True)
def test_old_spi_protocol(self):
# flash firmware with old SPI protocol
self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin"))
self._run_test(45)
def test_release_to_devel_bootstub(self):
self._flash_bootstub_and_test(None)
self._flash_bootstub(None)
self._run_test(45)
def test_recover_from_bad_bootstub(self):
self._go_to_dfu()

View File

@@ -9,7 +9,7 @@ from pprint import pprint
import cereal.messaging as messaging
from cereal import car, log
from opendbc.car.can_definitions import CanData
from openpilot.common.retry import retry
from openpilot.common.utils import retry
from openpilot.common.params import Params
from openpilot.common.timeout import Timeout
from openpilot.selfdrive.pandad import can_list_to_can_capnp

View File

@@ -41,6 +41,10 @@
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
"severity": 0
},
"Offroad_DriverMonitoringUncertain": {
"text": "openpilot detected poor visibility for driver monitoring. Ensure the device has a clear view of the driver. This can be checked using Settings -> Device -> Driver Camera Preview. Extreme lighting conditions and/or unconventional mounting positions may also trigger this alert.",
"severity": 0
},
"Offroad_ExcessiveActuation": {
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
"severity": 1,

View File

@@ -80,7 +80,7 @@ def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.
def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
return Alert(
f"Steer Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
@@ -322,7 +322,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.steerTempUnavailableSilent: {
ET.WARNING: Alert(
"Steering Temporarily Unavailable",
"Steering Assist Temporarily Unavailable",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
@@ -568,7 +568,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
},
EventName.steerTempUnavailable: {
ET.SOFT_DISABLE: soft_disable_alert("Steering Temporarily Unavailable"),
ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"),
ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"),
},

View File

@@ -1 +1 @@
afcab1abb62b9d5678342956cced4712f44e909e
b508f43fb0481bce0859c9b6ab4f45ee690b8dab

View File

@@ -42,6 +42,7 @@ sudo systemctl restart NetworkManager
sudo systemctl disable ssh-param-watcher.path
sudo systemctl disable ssh-param-watcher.service
sudo mount -o ro,remount /
sudo systemctl stop power_monitor
while true; do
if ! sudo systemctl is-active -q ssh; then
@@ -54,7 +55,6 @@ while true; do
# /data/ciui.py &
#fi
awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog
sleep 5s
done

View File

@@ -32,7 +32,7 @@ CPU usage budget
TEST_DURATION = 25
LOG_OFFSET = 8
MAX_TOTAL_CPU = 300. # total for all 8 cores
MAX_TOTAL_CPU = 350. # total for all 8 cores
PROCS = {
# Baseline CPU usage by process
"selfdrive.controls.controlsd": 16.0,
@@ -42,7 +42,7 @@ PROCS = {
"./encoderd": 13.0,
"./camerad": 10.0,
"selfdrive.controls.plannerd": 8.0,
"./ui": 18.0,
"selfdrive.ui.ui": 40.0,
"system.sensord.sensord": 13.0,
"selfdrive.controls.radard": 2.0,
"selfdrive.modeld.modeld": 22.0,
@@ -206,7 +206,8 @@ class TestOnroad:
result += "-------------- UI Draw Timing ------------------\n"
result += "------------------------------------------------\n"
ts = self.ts['uiDebug']['drawTimeMillis']
# skip first few frames -- connecting to vipc
ts = self.ts['uiDebug']['drawTimeMillis'][15:]
result += f"min {min(ts):.2f}ms\n"
result += f"max {max(ts):.2f}ms\n"
result += f"std {np.std(ts):.2f}ms\n"
@@ -215,7 +216,7 @@ class TestOnroad:
print(result)
assert max(ts) < 250.
assert np.mean(ts) < 10.
assert np.mean(ts) < 20. # TODO: ~6-11ms, increase consistency
#self.assertLess(np.std(ts), 5.)
# some slow frames are expected since camerad/modeld can preempt ui
@@ -285,7 +286,7 @@ class TestOnroad:
# check for big leaks. note that memory usage is
# expected to go up while the MSGQ buffers fill up
assert np.average(mems) <= 65, "Average memory usage above 65%"
assert np.average(mems) <= 85, "Average memory usage above 85%"
assert np.max(np.diff(mems)) <= 4, "Max memory increase too high"
assert np.average(np.diff(mems)) <= 1, "Average memory increase too high"

View File

@@ -1,14 +1 @@
moc_*
*.moc
translations/main_test_en.*
ui
mui
watch3
installer/installers/*
qt/setup/setup
qt/setup/reset
qt/setup/wifi
qt/setup/updater
translations/alerts_generated.h

View File

@@ -1,75 +1,37 @@
import os
import re
import json
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations')
from pathlib import Path
Import('env', 'arch', 'common')
base_libs = [common, messaging, visionipc, transformations,
'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
# build the fonts
generator = File("#selfdrive/assets/fonts/process.py")
source_files = Glob("#selfdrive/assets/fonts/*.ttf") + Glob("#selfdrive/assets/fonts/*.otf")
output_files = [
(f.abspath.split('.')[0] + ".fnt", f.abspath.split('.')[0] + ".png")
for f in source_files
if "NotoColor" not in f.name
]
env.Command(
target=output_files,
source=[generator, source_files],
action=f"python3 {generator}",
)
if arch == 'larch64':
base_libs.append('EGL')
if arch == "Darwin":
del base_libs[base_libs.index('OpenCL')]
qt_env['FRAMEWORKS'] += ['OpenCL']
sp_widgets_src = []
sp_qt_src = []
sp_qt_util = []
if not GetOption('stock_ui'):
SConscript(['sunnypilot/SConscript'])
Import('sp_widgets_src', 'sp_qt_src', 'sp_qt_util')
# FIXME: remove this once we're on 5.15 (24.04)
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"] + sp_qt_util, LIBS=base_libs)
widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc",
"qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc",
"qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc",
"qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc",
"qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
Export('widgets')
qt_libs = [widgets, qt_util] + base_libs
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/offroad_home.cc",
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc",
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",
"qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc",
"qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src
# build translation files
# compile gettext .po -> .mo translations
with open(File("translations/languages.json").abspath) as f:
languages = json.loads(f.read())
translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()]
translation_targets = [src.replace(".ts", ".qm") for src in translation_sources]
lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease'
lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES")
qt_env.NoClean(translation_sources)
qt_env.Precious(translation_sources)
po_sources = [f"#selfdrive/ui/translations/app_{l}.po" for l in languages.values()]
po_sources = [src for src in po_sources if os.path.exists(File(src).abspath)]
mo_targets = [src.replace(".po", ".mo") for src in po_sources]
mo_build = []
for src, tgt in zip(po_sources, mo_targets):
mo_build.append(env.Command(tgt, src, "msgfmt -o $TARGET $SOURCE"))
mo_alias = env.Alias('mo', mo_build)
env.AlwaysBuild(mo_alias)
# create qrc file for compiled translations to include with assets
translations_assets_src = "#selfdrive/assets/translations_assets.qrc"
with open(File(translations_assets_src).abspath, 'w') as f:
f.write('<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n')
f.write('\n'.join([f'<file alias="{l}">../ui/translations/{l}.qm</file>' for l in languages.values()]))
f.write('\n</qresource>\n</RCC>')
# build assets
assets = "#selfdrive/assets/assets.cc"
assets_src = "#selfdrive/assets/assets.qrc"
qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET")
qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease])
asset_obj = qt_env.Object("assets", assets)
# build main UI
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs)
if GetOption('extras'):
qt_src.remove("main.cc") # replaced by test_runner
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
# build installers
if arch != "Darwin":
raylib_env = env.Clone()
@@ -78,7 +40,7 @@ if GetOption('extras'):
raylib_libs = common + ["raylib"]
if arch == "larch64":
raylib_libs += ["GLESv2", "wayland-client", "wayland-egl", "EGL"]
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
else:
raylib_libs += ["GL"]

View File

@@ -0,0 +1 @@
UI_BORDER_SIZE = 30

View File

@@ -5,6 +5,7 @@
#include "common/swaglog.h"
#include "common/util.h"
#include "system/hardware/hw.h"
#include "third_party/raylib/include/raylib.h"
int freshClone();
@@ -38,6 +39,27 @@ extern const uint8_t inter_ttf_end[] asm("_binary_selfdrive_ui_installer_inter_a
Font font;
std::vector<std::string> tici_prebuilt_branches = {"release3", "release-tizi", "release3-staging", "nightly", "nightly-dev"};
std::string migrated_branch;
void branchMigration() {
migrated_branch = BRANCH_STR;
cereal::InitData::DeviceType device_type = Hardware::get_device_type();
if (device_type == cereal::InitData::DeviceType::TICI) {
if (std::find(tici_prebuilt_branches.begin(), tici_prebuilt_branches.end(), BRANCH_STR) != tici_prebuilt_branches.end()) {
migrated_branch = "release-tici";
} else if (BRANCH_STR == "master") {
migrated_branch = "master-tici";
}
} else if (device_type == cereal::InitData::DeviceType::TIZI) {
if (BRANCH_STR == "release3") {
migrated_branch = "release-tizi";
} else if (BRANCH_STR == "release3-staging") {
migrated_branch = "release-tizi-staging";
}
}
}
void run(const char* cmd) {
int err = std::system(cmd);
assert(err == 0);
@@ -87,7 +109,7 @@ int doInstall() {
int freshClone() {
LOGD("Doing fresh clone");
std::string cmd = util::string_format("git clone --progress %s -b %s --depth=1 --recurse-submodules %s 2>&1",
GIT_URL.c_str(), BRANCH_STR.c_str(), TMP_INSTALL_PATH);
GIT_URL.c_str(), migrated_branch.c_str(), TMP_INSTALL_PATH);
return executeGitCommand(cmd);
}
@@ -95,11 +117,11 @@ int cachedFetch(const std::string &cache) {
LOGD("Fetching with cache: %s", cache.c_str());
run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str());
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, BRANCH_STR.c_str()).c_str());
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, migrated_branch.c_str()).c_str());
renderProgress(10);
return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, BRANCH_STR.c_str()));
return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, migrated_branch.c_str()));
}
int executeGitCommand(const std::string &cmd) {
@@ -142,8 +164,8 @@ void cloneFinished(int exitCode) {
// ensure correct branch is checked out
int err = chdir(TMP_INSTALL_PATH);
assert(err == 0);
run(("git checkout " + BRANCH_STR).c_str());
run(("git reset --hard origin/" + BRANCH_STR).c_str());
run(("git checkout " + migrated_branch).c_str());
run(("git reset --hard origin/" + migrated_branch).c_str());
run("git submodule update --init");
// move into place
@@ -193,6 +215,8 @@ int main(int argc, char *argv[]) {
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0);
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
branchMigration();
if (util::file_exists(CONTINUE_PATH)) {
finishInstall();
} else {

View File

@@ -8,7 +8,9 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets import Widget
HEADER_HEIGHT = 80
@@ -35,12 +37,17 @@ class HomeLayout(Widget):
self.update_alert = UpdateAlert()
self.offroad_alert = OffroadAlert()
self._layout_widgets = {HomeLayoutState.UPDATE: self.update_alert, HomeLayoutState.ALERTS: self.offroad_alert}
self.current_state = HomeLayoutState.HOME
self.last_refresh = 0
self.settings_callback: callable | None = None
self.update_available = False
self.alert_count = 0
self._version_text = ""
self._prev_update_available = False
self._prev_alerts_present = False
self.header_rect = rl.Rectangle(0, 0, 0, 0)
self.content_rect = rl.Rectangle(0, 0, 0, 0)
@@ -56,14 +63,30 @@ class HomeLayout(Widget):
self._exp_mode_button = ExperimentalModeButton()
self._setup_callbacks()
def show_event(self):
self._exp_mode_button.show_event()
self.last_refresh = time.monotonic()
self._refresh()
def _setup_callbacks(self):
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
def set_settings_callback(self, callback: Callable):
self.settings_callback = callback
def _set_state(self, state: HomeLayoutState):
# propagate show/hide events
if state != self.current_state:
if state == HomeLayoutState.HOME:
self._exp_mode_button.show_event()
if state in self._layout_widgets:
self._layout_widgets[state].show_event()
if self.current_state in self._layout_widgets:
self._layout_widgets[self.current_state].hide_event()
self.current_state = state
def _render(self, rect: rl.Rectangle):
@@ -72,7 +95,6 @@ class HomeLayout(Widget):
self._refresh()
self.last_refresh = current_time
self._handle_input()
self._render_header()
# Render content based on current state
@@ -83,7 +105,7 @@ class HomeLayout(Widget):
elif self.current_state == HomeLayoutState.ALERTS:
self._render_alerts_view()
def _update_layout_rects(self):
def _update_state(self):
self.header_rect = rl.Rectangle(
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
)
@@ -110,59 +132,54 @@ class HomeLayout(Widget):
self.alert_notif_rect.x = notif_x
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
def _handle_input(self):
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
return
mouse_pos = rl.get_mouse_position()
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
self._set_state(HomeLayoutState.UPDATE)
return
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
elif self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
self._set_state(HomeLayoutState.ALERTS)
return
# Content area input handling
if self.current_state == HomeLayoutState.UPDATE:
self.update_alert.handle_input(mouse_pos, True)
elif self.current_state == HomeLayoutState.ALERTS:
self.offroad_alert.handle_input(mouse_pos, True)
def _render_header(self):
font = gui_app.font(FontWeight.MEDIUM)
version_text_width = self.header_rect.width
# Update notification button
if self.update_available:
version_text_width -= self.update_notif_rect.width
# Highlight if currently viewing updates
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255)
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
text = "UPDATE"
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
text = tr("UPDATE")
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
# Alert notification button
if self.alert_count > 0:
version_text_width -= self.alert_notif_rect.width
# Highlight if currently viewing alerts
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
# Version text (right aligned)
version_text = self._get_version_text()
text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x
version_x = self.header_rect.x + self.header_rect.width - text_width
version_y = self.header_rect.y + (self.header_rect.height - 48) // 2
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR)
if self.update_available or self.alert_count > 0:
version_text_width -= SPACING * 1.5
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
version_text_width, self.header_rect.height)
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
def _render_home_content(self):
self._render_left_column()
@@ -193,20 +210,23 @@ class HomeLayout(Widget):
self._setup_widget.render(setup_rect)
def _refresh(self):
# TODO: implement _update_state with a timer
self.update_available = self.update_alert.refresh()
self.alert_count = self.offroad_alert.refresh()
self._update_state_priority(self.update_available, self.alert_count > 0)
def _update_state_priority(self, update_available: bool, alerts_present: bool):
current_state = self.current_state
self._version_text = self._get_version_text()
update_available = self.update_alert.refresh()
alert_count = self.offroad_alert.refresh()
alerts_present = alert_count > 0
# Show panels on transition from no alert/update to any alerts/update
if not update_available and not alerts_present:
self.current_state = HomeLayoutState.HOME
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)):
self.current_state = HomeLayoutState.UPDATE
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)):
self.current_state = HomeLayoutState.ALERTS
self._set_state(HomeLayoutState.HOME)
elif update_available and ((not self._prev_update_available) or (not alerts_present and self.current_state == HomeLayoutState.ALERTS)):
self._set_state(HomeLayoutState.UPDATE)
elif alerts_present and ((not self._prev_alerts_present) or (not update_available and self.current_state == HomeLayoutState.UPDATE)):
self._set_state(HomeLayoutState.ALERTS)
self.update_available = update_available
self.alert_count = alert_count
self._prev_update_available = update_available
self._prev_alerts_present = alerts_present
def _get_version_text(self) -> str:
brand = "openpilot"

View File

@@ -1,12 +1,19 @@
import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
from openpilot.common.params import Params
if Params().get_bool("sunnypilot_ui"):
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.home import HomeLayoutSP as HomeLayout
class MainState(IntEnum):
@@ -34,16 +41,23 @@ class MainLayout(Widget):
# Set callbacks
self._setup_callbacks()
# Start onboarding if terms or training not completed
self._onboarding_window = OnboardingWindow()
if not self._onboarding_window.completed:
gui_app.set_modal_overlay(self._onboarding_window)
def _render(self, _):
self._handle_onroad_transition()
self._render_main_content()
def _setup_callbacks(self):
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
on_flag=self._on_bookmark_clicked)
on_flag=self._on_bookmark_clicked,
open_settings=lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state)
def _update_layout_rects(self):

View File

@@ -0,0 +1,214 @@
import os
import re
import threading
from enum import IntEnum
import pyray as rl
from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
from openpilot.selfdrive.ui.ui_state import ui_state
DEBUG = False
STEP_RECTS = [rl.Rectangle(104, 800, 633, 175), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2156, 1080),
rl.Rectangle(1526, 473, 427, 472), rl.Rectangle(1643, 441, 217, 223), rl.Rectangle(1835, 0, 2155, 1080),
rl.Rectangle(1786, 591, 267, 236), rl.Rectangle(1353, 0, 804, 1080), rl.Rectangle(1458, 485, 633, 211),
rl.Rectangle(95, 794, 1158, 187), rl.Rectangle(1560, 170, 392, 397), rl.Rectangle(1835, 0, 2159, 1080),
rl.Rectangle(1351, 0, 807, 1080), rl.Rectangle(1835, 0, 2158, 1080), rl.Rectangle(1531, 82, 441, 920),
rl.Rectangle(1336, 438, 490, 393), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2159, 1080),
rl.Rectangle(87, 795, 1187, 186)]
DM_RECORD_STEP = 9
DM_RECORD_YES_RECT = rl.Rectangle(695, 794, 558, 187)
RESTART_TRAINING_RECT = rl.Rectangle(87, 795, 472, 186)
class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
class TrainingGuide(Widget):
def __init__(self, completed_callback=None):
super().__init__()
self._completed_callback = completed_callback
self._step = 0
self._load_image_paths()
# Load first image now so we show something immediately
self._textures = [gui_app.texture(self._image_paths[0])]
self._image_objs = []
threading.Thread(target=self._preload_thread, daemon=True).start()
def _load_image_paths(self):
paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)]
paths = sorted(paths, key=lambda x: int(re.search(r'\d+', x).group()))
self._image_paths = [os.path.join(BASEDIR, "selfdrive/assets/training", fn) for fn in paths]
def _preload_thread(self):
# PNG loading is slow in raylib, so we preload in a thread and upload to GPU in main thread
# We've already loaded the first image on init
for path in self._image_paths[1:]:
self._image_objs.append(gui_app._load_image_from_path(path))
def _handle_mouse_release(self, mouse_pos):
if rl.check_collision_point_rec(mouse_pos, STEP_RECTS[self._step]):
# Record DM camera?
if self._step == DM_RECORD_STEP:
yes = rl.check_collision_point_rec(mouse_pos, DM_RECORD_YES_RECT)
print(f"putting RecordFront to {yes}")
ui_state.params.put_bool("RecordFront", yes)
# Restart training?
elif self._step == len(self._image_paths) - 1:
if rl.check_collision_point_rec(mouse_pos, RESTART_TRAINING_RECT):
self._step = -1
self._step += 1
# Finished?
if self._step >= len(self._image_paths):
self._step = 0
if self._completed_callback:
self._completed_callback()
def _update_state(self):
if len(self._image_objs):
self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0)))
def _render(self, _):
# Safeguard against fast tapping
step = min(self._step, len(self._textures) - 1)
rl.draw_texture(self._textures[step], 0, 0, rl.WHITE)
# progress bar
if 0 < step < len(STEP_RECTS) - 1:
h = 20
w = int((step / (len(STEP_RECTS) - 1)) * self._rect.width)
rl.draw_rectangle(int(self._rect.x), int(self._rect.y + self._rect.height - h),
w, h, rl.Color(70, 91, 234, 255))
if DEBUG:
rl.draw_rectangle_lines_ex(STEP_RECTS[step], 3, rl.RED)
return -1
class TermsPage(Widget):
def __init__(self, on_accept=None, on_decline=None):
super().__init__()
self._on_accept = on_accept
self._on_decline = on_decline
self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
def _render(self, _):
welcome_x = self._rect.x + 165
welcome_y = self._rect.y + 165
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
self._title.render(welcome_rect)
desc_x = welcome_x
# TODO: Label doesn't top align when wrapping
desc_y = welcome_y - 100
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
self._desc.render(desc_rect)
btn_y = self._rect.y + self._rect.height - 160 - 45
btn_width = (self._rect.width - 45 * 3) / 2
self._decline_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
self._accept_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
if DEBUG:
rl.draw_rectangle_lines_ex(welcome_rect, 3, rl.RED)
rl.draw_rectangle_lines_ex(desc_rect, 3, rl.RED)
return -1
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
click_callback=self._on_uninstall_clicked)
def _on_uninstall_clicked(self):
ui_state.params.put_bool("DoUninstall", True)
gui_app.request_close()
def _render(self, _):
btn_y = self._rect.y + self._rect.height - 160 - 45
btn_width = (self._rect.width - 45 * 3) / 2
self._back_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
self._uninstall_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
# text rect in middle of top and button
text_height = btn_y - (200 + 45)
text_rect = rl.Rectangle(self._rect.x + 165, self._rect.y + (btn_y - text_height) / 2 + 10, self._rect.width - (165 * 2), text_height)
if DEBUG:
rl.draw_rectangle_lines_ex(text_rect, 3, rl.RED)
self._text.render(text_rect)
class OnboardingWindow(Widget):
def __init__(self):
super().__init__()
self._current_terms_version = ui_state.params.get("TermsVersion")
self._current_training_version = ui_state.params.get("TrainingVersion")
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == self._current_terms_version
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == self._current_training_version
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
# Windows
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
self._training_guide: TrainingGuide | None = None
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
@property
def completed(self) -> bool:
return self._accepted_terms and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
def _on_decline_back(self):
self._state = OnboardingState.TERMS
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", self._current_terms_version)
self._state = OnboardingState.ONBOARDING
if self._training_done:
gui_app.set_modal_overlay(None)
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", self._current_training_version)
gui_app.set_modal_overlay(None)
def _render(self, _):
if self._training_guide is None:
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
if self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1

View File

@@ -1,20 +1,33 @@
from openpilot.common.params import Params
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import toggle_item
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
if Params().get_bool("sunnypilot_ui"):
from openpilot.system.ui.sunnypilot.lib.list_view import (toggle_item_sp as toggle_item)
# Description constants
DESCRIPTIONS = {
'enable_adb': (
'enable_adb': tr_noop(
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
),
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
'ssh_key': (
'ssh_key': tr_noop(
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
),
'alpha_longitudinal': tr_noop(
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
"On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " +
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. " +
"Changing this setting will restart openpilot if the car is powered on."
),
}
@@ -22,40 +35,154 @@ class DeveloperLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = [
toggle_item(
"Enable ADB",
description=DESCRIPTIONS["enable_adb"],
initial_state=self._params.get_bool("AdbEnabled"),
callback=self._on_enable_adb,
),
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
toggle_item(
"Joystick Debug Mode",
description=DESCRIPTIONS["joystick_debug_mode"],
initial_state=self._params.get_bool("JoystickDebugMode"),
callback=self._on_joystick_debug_mode,
),
toggle_item(
"Longitudinal Maneuver Mode",
description="",
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
callback=self._on_long_maneuver_mode,
),
toggle_item(
"openpilot Longitudinal Control (Alpha)",
description="",
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
callback=self._on_alpha_long_enabled,
),
]
self._is_release = self._params.get_bool("IsReleaseBranch")
self._scroller = Scroller(items, line_separator=True, spacing=0)
# Build items and keep references for callbacks/state updates
self._adb_toggle = toggle_item(
lambda: tr("Enable ADB"),
description=lambda: tr(DESCRIPTIONS["enable_adb"]),
initial_state=self._params.get_bool("AdbEnabled"),
callback=self._on_enable_adb,
enabled=ui_state.is_offroad,
)
# SSH enable toggle + SSH key management
self._ssh_toggle = toggle_item(
lambda: tr("Enable SSH"),
description="",
initial_state=self._params.get_bool("SshEnabled"),
callback=self._on_enable_ssh,
)
self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: tr(DESCRIPTIONS["ssh_key"]))
self._joystick_toggle = toggle_item(
lambda: tr("Joystick Debug Mode"),
description="",
initial_state=self._params.get_bool("JoystickDebugMode"),
callback=self._on_joystick_debug_mode,
enabled=ui_state.is_offroad,
)
self._long_maneuver_toggle = toggle_item(
lambda: tr("Longitudinal Maneuver Mode"),
description="",
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
callback=self._on_long_maneuver_mode,
)
self._alpha_long_toggle = toggle_item(
lambda: tr("openpilot Longitudinal Control (Alpha)"),
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
callback=self._on_alpha_long_enabled,
enabled=lambda: not ui_state.engaged,
)
self._ui_debug_toggle = toggle_item(
lambda: tr("UI Debug Mode"),
description="",
initial_state=self._params.get_bool("ShowDebugInfo"),
callback=self._on_enable_ui_debug,
)
self._on_enable_ui_debug(self._params.get_bool("ShowDebugInfo"))
self._scroller = Scroller([
self._adb_toggle,
self._ssh_toggle,
self._ssh_keys,
self._joystick_toggle,
self._long_maneuver_toggle,
self._alpha_long_toggle,
self._ui_debug_toggle,
], line_separator=True, spacing=0)
# Toggles should be not available to change in onroad state
ui_state.add_offroad_transition_callback(self._update_toggles)
def _render(self, rect):
self._scroller.render(rect)
def _on_enable_adb(self): pass
def _on_joystick_debug_mode(self): pass
def _on_long_maneuver_mode(self): pass
def _on_alpha_long_enabled(self): pass
def show_event(self):
self._scroller.show_event()
self._update_toggles()
def _update_toggles(self):
ui_state.update_params()
# Hide non-release toggles on release builds
# TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault
for item in (self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle):
item.set_visible(not self._is_release)
# CP gating
if ui_state.CP is not None:
alpha_avail = ui_state.CP.alphaLongitudinalAvailable
if not alpha_avail or self._is_release:
self._alpha_long_toggle.set_visible(False)
self._params.remove("AlphaLongitudinalEnabled")
else:
self._alpha_long_toggle.set_visible(True)
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
if not long_man_enabled:
self._long_maneuver_toggle.action_item.set_state(False)
self._params.put_bool("LongitudinalManeuverMode", False)
else:
self._long_maneuver_toggle.action_item.set_enabled(False)
self._alpha_long_toggle.set_visible(False)
# TODO: make a param control list item so we don't need to manage internal state as much here
# refresh toggles from params to mirror external changes
for key, item in (
("AdbEnabled", self._adb_toggle),
("SshEnabled", self._ssh_toggle),
("JoystickDebugMode", self._joystick_toggle),
("LongitudinalManeuverMode", self._long_maneuver_toggle),
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
("ShowDebugInfo", self._ui_debug_toggle),
):
item.action_item.set_state(self._params.get_bool(key))
def _on_enable_ui_debug(self, state: bool):
self._params.put_bool("ShowDebugInfo", state)
gui_app.set_show_touches(state)
gui_app.set_show_fps(state)
def _on_enable_adb(self, state: bool):
self._params.put_bool("AdbEnabled", state)
def _on_enable_ssh(self, state: bool):
self._params.put_bool("SshEnabled", state)
def _on_joystick_debug_mode(self, state: bool):
self._params.put_bool("JoystickDebugMode", state)
self._params.put_bool("LongitudinalManeuverMode", False)
self._long_maneuver_toggle.action_item.set_state(False)
def _on_long_maneuver_mode(self, state: bool):
self._params.put_bool("LongitudinalManeuverMode", state)
self._params.put_bool("JoystickDebugMode", False)
self._joystick_toggle.action_item.set_state(False)
def _on_alpha_long_enabled(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
self._params.put_bool("AlphaLongitudinalEnabled", True)
self._params.put_bool("OnroadCycleRequested", True)
self._update_toggles()
else:
self._alpha_long_toggle.action_item.set_state(False)
# show confirmation dialog
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
f"<p>{self._alpha_long_toggle.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
else:
self._params.put_bool("AlphaLongitudinalEnabled", False)
self._params.put_bool("OnroadCycleRequested", True)
self._update_toggles()

View File

@@ -1,29 +1,30 @@
import os
import json
import math
from cereal import messaging, log
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.html_render import HtmlModal
from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller import Scroller
# Description constants
DESCRIPTIONS = {
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
'reset_calibration': (
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
),
'review_guide': "Review the rules, features, and limitations of openpilot",
'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
}
@@ -35,49 +36,61 @@ class DeviceLayout(Widget):
self._select_language_dialog: MultiOptionDialog | None = None
self._driver_camera: DriverCameraDialog | None = None
self._pair_device_dialog: PairingDialog | None = None
self._fcc_dialog: HtmlRenderer | None = None
self._fcc_dialog: HtmlModal | None = None
self._training_guide: TrainingGuide | None = None
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
ui_state.add_offroad_transition_callback(self._offroad_transition)
def _initialize_items(self):
dongle_id = self._params.get("DongleId") or "N/A"
serial = self._params.get("HardwareSerial") or "N/A"
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device)
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
callback=self._reset_calibration_prompt)
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
self._power_off_btn = dual_button_item(lambda: tr("Reboot"), lambda: tr("Power Off"),
left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
items = [
text_item("Dongle ID", dongle_id),
text_item("Serial", serial),
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
self._pair_device_btn,
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
self._reset_calib_btn,
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
self._on_review_training_guide, enabled=ui_state.is_offroad),
regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
self._power_off_btn,
]
regulatory_btn.set_visible(TICI)
return items
def _offroad_transition(self):
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
def show_event(self):
self._scroller.show_event()
def _render(self, rect):
self._scroller.render(rect)
def _show_language_selection(self):
try:
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
with open(languages_file, encoding='utf-8') as f:
languages = json.load(f)
def _show_language_dialog(self):
def handle_language_selection(result: int):
if result == 1 and self._select_language_dialog:
selected_language = multilang.languages[self._select_language_dialog.selection]
multilang.change_language(selected_language)
self._update_calib_description()
self._select_language_dialog = None
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
except FileNotFoundError:
pass
def _handle_language_selection(self, result: int):
if result == 1 and self._select_language_dialog:
selected_language = self._select_language_dialog.selection
self._params.put("LanguageSetting", selected_language)
self._select_language_dialog = None
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
option_font_weight=FontWeight.UNIFONT)
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
def _show_driver_camera(self):
if not self._driver_camera:
@@ -87,34 +100,80 @@ class DeviceLayout(Widget):
def _reset_calibration_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration"))
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
return
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"),
callback=self._reset_calibration,
)
def reset_calibration(result: int):
# Check engaged again in case it changed while the dialog was open
if ui_state.engaged or result != DialogResult.CONFIRM:
return
def _reset_calibration(self, result: int):
if ui_state.engaged or result != DialogResult.CONFIRM:
return
self._params.remove("CalibrationParams")
self._params.remove("LiveTorqueParameters")
self._params.remove("LiveParameters")
self._params.remove("LiveParametersV2")
self._params.remove("LiveDelay")
self._params.put_bool("OnroadCycleRequested", True)
self._update_calib_description()
self._params.remove("CalibrationParams")
self._params.remove("LiveTorqueParameters")
self._params.remove("LiveParameters")
self._params.remove("LiveParametersV2")
self._params.remove("LiveDelay")
self._params.put_bool("OnroadCycleRequested", True)
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
def _update_calib_description(self):
desc = tr(DESCRIPTIONS['reset_calibration'])
calib_bytes = self._params.get("CalibrationParams")
if calib_bytes:
try:
calib = messaging.log_from_bytes(calib_bytes, log.Event).liveCalibration
if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated:
pitch = math.degrees(calib.rpyCalib[1])
yaw = math.degrees(calib.rpyCalib[2])
desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"),
abs(yaw), tr("left") if yaw > 0 else tr("right"))
except Exception:
cloudlog.exception("invalid CalibrationParams")
lag_perc = 0
lag_bytes = self._params.get("LiveDelay")
if lag_bytes:
try:
lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).liveDelay.calPerc
except Exception:
cloudlog.exception("invalid LiveDelay")
if lag_perc < 100:
desc += tr("<br><br>Steering lag calibration is {}% complete.").format(lag_perc)
else:
desc += tr("<br><br>Steering lag calibration is complete.")
torque_bytes = self._params.get("LiveTorqueParameters")
if torque_bytes:
try:
torque = messaging.log_from_bytes(torque_bytes, log.Event).liveTorqueParameters
# don't add for non-torque cars
if torque.useParams:
torque_perc = torque.calPerc
if torque_perc < 100:
desc += tr(" Steering torque response calibration is {}% complete.").format(torque_perc)
else:
desc += tr(" Steering torque response calibration is complete.")
except Exception:
cloudlog.exception("invalid LiveTorqueParameters")
desc += "<br><br>"
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
"Resetting calibration will restart openpilot if the car is powered on.")
self._reset_calib_btn.set_description(desc)
def _reboot_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot"))
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot")))
return
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"),
callback=self._perform_reboot,
)
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
def _perform_reboot(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
@@ -122,13 +181,11 @@ class DeviceLayout(Widget):
def _power_off_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off"))
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
return
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"),
callback=self._perform_power_off,
)
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
def _perform_power_off(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
@@ -141,10 +198,13 @@ class DeviceLayout(Widget):
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)
gui_app.set_modal_overlay(self._fcc_dialog,
callback=lambda result: setattr(self, '_fcc_dialog', None),
)
def _on_review_training_guide(self):
if not self._training_guide:
def completed_callback():
gui_app.set_modal_overlay(None)
def _on_review_training_guide(self): pass
self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide)

View File

@@ -7,21 +7,23 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.lib.api_helpers import get_token
TITLE = "Firehose Mode"
DESCRIPTION = (
TITLE = tr_noop("Firehose Mode")
DESCRIPTION = tr_noop(
"openpilot learns to drive by watching humans, like you, drive.\n\n"
+ "Firehose Mode allows you to maximize your training data uploads to improve "
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
)
INSTRUCTIONS = (
INSTRUCTIONS = tr_noop(
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
+ "Frequently Asked Questions\n\n"
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
@@ -43,12 +45,16 @@ class FirehoseLayout(Widget):
self.params = Params()
self.segment_count = self._get_segment_count()
self.scroll_panel = GuiScrollPanel()
self._content_height = 0
self.running = True
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
self.update_thread.start()
self.last_update_time = 0
def show_event(self):
self.scroll_panel.set_offset(0)
def _get_segment_count(self) -> int:
stats = self.params.get(self.PARAM_KEY)
if not stats:
@@ -66,97 +72,72 @@ class FirehoseLayout(Widget):
def _render(self, rect: rl.Rectangle):
# Calculate content dimensions
content_width = rect.width - 80
content_height = self._calculate_content_height(int(content_width))
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
# Handle scrolling and render with clipping
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
scroll_offset = self.scroll_panel.update(rect, content_rect)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._render_content(rect, scroll_offset)
self._content_height = self._render_content(rect, scroll_offset)
rl.end_scissor_mode()
def _calculate_content_height(self, content_width: int) -> int:
height = 80 # Top margin
# Title
height += 100 + 40
# Description
desc_font = gui_app.font(FontWeight.NORMAL)
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
height += len(desc_lines) * 45 + 40
# Status section
height += 32 # Separator
status_text, _ = self._get_status()
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
height += len(status_lines) * 60 + 20
# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
height += len(contrib_lines) * 52 + 20
# Instructions section
height += 32 # Separator
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
height += len(inst_lines) * 40 + 40 # Bottom margin
return height
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
x = int(rect.x + 40)
y = int(rect.y + 40 + scroll_offset.y)
y = int(rect.y + 40 + scroll_offset)
w = int(rect.width - 80)
# Title
# Title (centered)
title_text = tr(TITLE) # live translate
title_font = gui_app.font(FontWeight.MEDIUM)
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
y += 140
text_width = measure_text_cached(title_font, title_text, 100).x
title_x = rect.x + (rect.width - text_width) / 2
rl.draw_text_ex(title_font, title_text, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
y += 200
# Description
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
y += 40
y = self._draw_wrapped_text(x, y, w, tr(DESCRIPTION), gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
y += 40 + 20
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
y += 30 + 20
# Status
status_text, status_color = self._get_status()
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
y += 20
y += 20 + 20
# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20
y += 20 + 20
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
y += 30 + 20
# Instructions
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
wrapped = wrap_text(font, text, size, width)
# bottom margin + remove effect of scroll offset
return int(round(y - self.scroll_panel.offset + 40))
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
wrapped = wrap_text(font, text, font_size, width)
for line in wrapped:
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
y += size
return y
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
y += font_size * FONT_SCALE
return round(y)
def _get_status(self) -> tuple[str, rl.Color]:
network_type = ui_state.sm["deviceState"].networkType
network_metered = ui_state.sm["deviceState"].networkMetered
if not network_metered and network_type != 0: # Not metered and connected
return "ACTIVE", self.GREEN
return tr("ACTIVE"), self.GREEN
else:
return "INACTIVE: connect to an unmetered network", self.RED
return tr("INACTIVE: connect to an unmetered network"), self.RED
def _fetch_firehose_stats(self):
try:

View File

@@ -8,18 +8,16 @@ from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import WifiManagerUI
# Settings close button
SETTINGS_CLOSE_TEXT = "×"
SETTINGS_CLOSE_TEXT_Y_OFFSET = 8 # The '×' character isn't quite vertically centered in the font so we need to offset it a bit to fully center it
from openpilot.system.ui.widgets.network import NetworkUI
# Constants
SIDEBAR_WIDTH = 500
CLOSE_BTN_SIZE = 200
CLOSE_ICON_SIZE = 70
NAV_BTN_HEIGHT = 110
PANEL_MARGIN = 50
@@ -58,15 +56,16 @@ class SettingsLayout(Widget):
wifi_manager.set_active(False)
self._panels = {
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)),
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()),
PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)),
PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
}
self._font_medium = gui_app.font(FontWeight.MEDIUM)
self._close_icon = gui_app.texture("icons/close2.png", CLOSE_ICON_SIZE, CLOSE_ICON_SIZE)
# Callbacks
self._close_callback: Callable | None = None
@@ -96,12 +95,21 @@ class SettingsLayout(Widget):
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
close_text_size = measure_text_cached(self._font_medium, SETTINGS_CLOSE_TEXT, 140)
close_text_pos = rl.Vector2(
close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2,
close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2 - SETTINGS_CLOSE_TEXT_Y_OFFSET,
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
icon_dest = rl.Rectangle(
close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2,
close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2,
self._close_icon.width,
self._close_icon.height,
)
rl.draw_texture_pro(
self._close_icon,
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
icon_dest,
rl.Vector2(0, 0),
0,
icon_color,
)
rl.draw_text_ex(self._font_medium, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED)
# Store close button rect for click detection
self._close_btn_rect = close_btn_rect
@@ -115,11 +123,12 @@ class SettingsLayout(Widget):
is_selected = panel_type == self._current_panel
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
# Draw button text (right-aligned)
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
panel_name = tr(panel_info.name)
text_size = measure_text_cached(self._font_medium, panel_name, 65)
text_pos = rl.Vector2(
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
)
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
rl.draw_text_ex(self._font_medium, panel_name, text_pos, 65, 0, text_color)
# Store button rect for click detection
panel_info.button_rect = button_rect

View File

@@ -1,42 +1,194 @@
from openpilot.common.params import Params
import os
import time
import datetime
from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
from openpilot.system.ui.widgets.list_view import button_item, text_item
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller import Scroller
# TODO: remove this. updater fails to respond on startup if time is not correct
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
def time_ago(date: datetime.datetime | None) -> str:
if not date:
return tr("never")
if not system_time_valid():
return date.strftime("%a %b %d %Y")
now = datetime.datetime.now(datetime.UTC)
if date.tzinfo is None:
date = date.replace(tzinfo=datetime.UTC)
diff_seconds = int((now - date).total_seconds())
if diff_seconds < 60:
return tr("now")
if diff_seconds < 3600:
m = diff_seconds // 60
return trn("{} minute ago", "{} minutes ago", m).format(m)
if diff_seconds < 86400:
h = diff_seconds // 3600
return trn("{} hour ago", "{} hours ago", h).format(h)
if diff_seconds < 604800:
d = diff_seconds // 86400
return trn("{} day ago", "{} days ago", d).format(d)
return date.strftime("%a %b %d %Y")
class SoftwareLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._init_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update)
def _init_items(self):
items = [
text_item("Current Version", ""),
button_item("Download", "CHECK", callback=self._on_download_update),
button_item("Install Update", "INSTALL", callback=self._on_install_update),
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
]
return items
# Install button is initially hidden
self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update)
self._install_btn.set_visible(False)
# Track waiting-for-updater transition to avoid brief re-enable while still idle
self._waiting_for_updater = False
self._waiting_start_ts: float = 0.0
# Branch switcher
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
self._branch_dialog: MultiOptionDialog | None = None
self._scroller = Scroller([
self._onroad_label,
self._version_item,
self._download_btn,
self._install_btn,
self._branch_btn,
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
], line_separator=True, spacing=0)
def show_event(self):
self._scroller.show_event()
def _render(self, rect):
self._scroller.render(rect)
def _on_download_update(self): pass
def _on_install_update(self): pass
def _on_select_branch(self): pass
def _update_state(self):
# Show/hide onroad warning
self._onroad_label.set_visible(ui_state.is_onroad())
# Update current version and release notes
current_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
self._version_item.action_item.set_text(current_desc)
self._version_item.set_description(current_release_notes)
# Update download button visibility and state
self._download_btn.set_visible(ui_state.is_offroad())
updater_state = ui_state.params.get("UpdaterState") or "idle"
failed_count = ui_state.params.get("UpdateFailedCount") or 0
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
update_available = ui_state.params.get_bool("UpdateAvailable")
if updater_state != "idle":
# Updater responded
self._waiting_for_updater = False
self._download_btn.action_item.set_enabled(False)
self._download_btn.action_item.set_value(updater_state)
else:
if failed_count > 0:
self._download_btn.action_item.set_value(tr("failed to check for update"))
self._download_btn.action_item.set_text(tr("CHECK"))
elif fetch_available:
self._download_btn.action_item.set_value(tr("update available"))
self._download_btn.action_item.set_text(tr("DOWNLOAD"))
else:
last_update = ui_state.params.get("LastUpdateTime")
if last_update:
formatted = time_ago(last_update)
self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted))
else:
self._download_btn.action_item.set_value(tr("up to date, last checked never"))
self._download_btn.action_item.set_text(tr("CHECK"))
# If we've been waiting too long without a state change, reset state
if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT):
self._waiting_for_updater = False
# Only enable if we're not waiting for updater to flip out of idle
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
# Update target branch button value
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
self._branch_btn.action_item.set_value(current_branch)
# Update install button
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
if update_available:
new_desc = ui_state.params.get("UpdaterNewDescription") or ""
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
self._install_btn.action_item.set_text(tr("INSTALL"))
self._install_btn.action_item.set_value(new_desc)
self._install_btn.set_description(new_release_notes)
# Enable install button for testing (like Qt showEvent)
self._install_btn.action_item.set_enabled(True)
else:
self._install_btn.set_visible(False)
def _on_download_update(self):
# Check if we should start checking or start downloading
self._download_btn.action_item.set_enabled(False)
if self._download_btn.action_item.text == tr("CHECK"):
# Start checking for updates
self._waiting_for_updater = True
self._waiting_start_ts = time.monotonic()
os.system("pkill -SIGUSR1 -f system.updated.updated")
else:
# Start downloading
self._waiting_for_updater = True
self._waiting_start_ts = time.monotonic()
os.system("pkill -SIGHUP -f system.updated.updated")
def _on_uninstall(self):
def handle_uninstall_confirmation(result):
if result == DialogResult.CONFIRM:
self._params.put_bool("DoUninstall", True)
ui_state.params.put_bool("DoUninstall", True)
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"),
callback=handle_uninstall_confirmation,
)
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
def _on_install_update(self):
# Trigger reboot to install update
self._install_btn.action_item.set_enabled(False)
ui_state.params.put_bool("DoReboot", True)
def _on_select_branch(self):
# Get available branches and order
current_git_branch = ui_state.params.get("GitBranch") or ""
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
branches = [b for b in branches_str.split(",") if b]
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
if b in branches:
branches.remove(b)
branches.insert(0, b)
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target)
def handle_selection(result):
# Confirmed selection
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
selection = self._branch_dialog.selection
ui_state.params.put("UpdaterTargetBranch", selection)
self._branch_btn.action_item.set_value(selection)
os.system("pkill -SIGUSR1 -f system.updated.updated")
self._branch_dialog = None
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)

View File

@@ -1,28 +1,40 @@
from openpilot.common.params import Params
from cereal import log
from openpilot.common.params import Params, UnknownKeyName
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
if Params().get_bool("sunnypilot_ui"):
from openpilot.system.ui.sunnypilot.lib.list_view import (multiple_button_item_sp as multiple_button_item,
toggle_item_sp as toggle_item)
# Description constants
DESCRIPTIONS = {
"OpenpilotEnabledToggle": (
"OpenpilotEnabledToggle": tr_noop(
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
"Your attention is required at all times to use this feature."
),
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
"LongitudinalPersonality": (
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
"LongitudinalPersonality": tr_noop(
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
"your steering wheel distance button."
),
"IsLdwEnabled": (
"IsLdwEnabled": tr_noop(
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
"without a turn signal activated while driving over 31 mph (50 km/h)."
),
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
"IsMetric": "Display speed in km/h instead of mph.",
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.",
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
}
@@ -30,66 +42,207 @@ class TogglesLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = [
toggle_item(
"Enable openpilot",
DESCRIPTIONS["OpenpilotEnabledToggle"],
self._params.get_bool("OpenpilotEnabledToggle"),
icon="chffr_wheel.png",
),
toggle_item(
"Experimental Mode",
initial_state=self._params.get_bool("ExperimentalMode"),
icon="experimental_white.png",
),
toggle_item(
"Disengage on Accelerator Pedal",
DESCRIPTIONS["DisengageOnAccelerator"],
self._params.get_bool("DisengageOnAccelerator"),
icon="disengage_on_accelerator.png",
),
multiple_button_item(
"Driving Personality",
DESCRIPTIONS["LongitudinalPersonality"],
buttons=["Aggressive", "Standard", "Relaxed"],
button_width=255,
callback=self._set_longitudinal_personality,
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
icon="speed_limit.png"
),
toggle_item(
"Enable Lane Departure Warnings",
DESCRIPTIONS["IsLdwEnabled"],
self._params.get_bool("IsLdwEnabled"),
icon="warning.png",
),
toggle_item(
"Always-On Driver Monitoring",
DESCRIPTIONS["AlwaysOnDM"],
self._params.get_bool("AlwaysOnDM"),
icon="monitoring.png",
),
toggle_item(
"Record and Upload Driver Camera",
DESCRIPTIONS["RecordFront"],
self._params.get_bool("RecordFront"),
icon="monitoring.png",
),
toggle_item(
"Record Microphone Audio",
DESCRIPTIONS["RecordAudio"],
self._params.get_bool("RecordAudio"),
icon="microphone.png",
),
toggle_item(
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png"
),
]
self._is_release = self._params.get_bool("IsReleaseBranch")
self._scroller = Scroller(items, line_separator=True, spacing=0)
# param, title, desc, icon, needs_restart
self._toggle_defs = {
"OpenpilotEnabledToggle": (
lambda: tr("Enable openpilot"),
DESCRIPTIONS["OpenpilotEnabledToggle"],
"chffr_wheel.png",
True,
),
"ExperimentalMode": (
lambda: tr("Experimental Mode"),
"",
"experimental_white.png",
False,
),
"DisengageOnAccelerator": (
lambda: tr("Disengage on Accelerator Pedal"),
DESCRIPTIONS["DisengageOnAccelerator"],
"disengage_on_accelerator.png",
False,
),
"IsLdwEnabled": (
lambda: tr("Enable Lane Departure Warnings"),
DESCRIPTIONS["IsLdwEnabled"],
"warning.png",
False,
),
"AlwaysOnDM": (
lambda: tr("Always-On Driver Monitoring"),
DESCRIPTIONS["AlwaysOnDM"],
"monitoring.png",
False,
),
"RecordFront": (
lambda: tr("Record and Upload Driver Camera"),
DESCRIPTIONS["RecordFront"],
"monitoring.png",
True,
),
"RecordAudio": (
lambda: tr("Record and Upload Microphone Audio"),
DESCRIPTIONS["RecordAudio"],
"microphone.png",
True,
),
"IsMetric": (
lambda: tr("Use Metric System"),
DESCRIPTIONS["IsMetric"],
"metric.png",
False,
),
}
self._long_personality_setting = multiple_button_item(
lambda: tr("Driving Personality"),
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
button_width=255,
callback=self._set_longitudinal_personality,
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
icon="speed_limit.png"
)
self._toggles = {}
self._locked_toggles = set()
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
toggle = toggle_item(
title,
desc,
self._params.get_bool(param),
callback=lambda state, p=param: self._toggle_callback(state, p),
icon=icon,
)
try:
locked = self._params.get_bool(param + "Lock")
except UnknownKeyName:
locked = False
toggle.action_item.set_enabled(not locked)
# Make description callable for live translation
additional_desc = ""
if needs_restart and not locked:
additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.")
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
# track for engaged state updates
if locked:
self._locked_toggles.add(param)
self._toggles[param] = toggle
# insert longitudinal personality after NDOG toggle
if param == "DisengageOnAccelerator":
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
self._update_experimental_mode_icon()
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
ui_state.add_engaged_transition_callback(self._update_toggles)
def _update_state(self):
if ui_state.sm.updated["selfdriveState"]:
personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality]
if personality != ui_state.personality and ui_state.started:
self._long_personality_setting.action_item.set_selected_button(personality)
ui_state.personality = personality
def show_event(self):
self._scroller.show_event()
self._update_toggles()
def _update_toggles(self):
ui_state.update_params()
e2e_description = tr(
"openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
"Experimental features are listed below:<br>" +
"<h4>End-to-End Longitudinal Control</h4><br>" +
"Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " +
"mistakes should be expected.<br>" +
"<h4>New Driving Visualization</h4><br>" +
"The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " +
"The Experimental mode logo will also be shown in the top right corner."
)
if ui_state.CP is not None:
if ui_state.has_longitudinal_control:
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
self._toggles["ExperimentalMode"].set_description(e2e_description)
self._long_personality_setting.action_item.set_enabled(True)
else:
# no long for now
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
self._toggles["ExperimentalMode"].action_item.set_state(False)
self._long_personality_setting.action_item.set_enabled(False)
self._params.remove("ExperimentalMode")
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
if ui_state.CP.alphaLongitudinalAvailable:
if self._is_release:
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
"Experimental mode, on non-release branches.")
else:
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
else:
self._toggles["ExperimentalMode"].set_description(e2e_description)
self._update_experimental_mode_icon()
# TODO: make a param control list item so we don't need to manage internal state as much here
# refresh toggles from params to mirror external changes
for param in self._toggle_defs:
self._toggles[param].action_item.set_state(self._params.get_bool(param))
# these toggles need restart, block while engaged
for toggle_def in self._toggle_defs:
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged)
def _render(self, rect):
self._scroller.render(rect)
def _update_experimental_mode_icon(self):
icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
self._toggles["ExperimentalMode"].set_icon(icon)
def _handle_experimental_mode_toggle(self, state: bool):
confirmed = self._params.get_bool("ExperimentalModeConfirmed")
if state and not confirmed:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
self._params.put_bool("ExperimentalMode", True)
self._params.put_bool("ExperimentalModeConfirmed", True)
else:
self._toggles["ExperimentalMode"].action_item.set_state(False)
self._update_experimental_mode_icon()
# show confirmation dialog
content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
else:
self._update_experimental_mode_icon()
self._params.put_bool("ExperimentalMode", state)
def _toggle_callback(self, state: bool, param: str):
if param == "ExperimentalMode":
self._handle_experimental_mode_toggle(state)
return
self._params.put_bool(param, state)
if self._toggle_defs[param][3]:
self._params.put_bool("OnroadCycleRequested", True)
def _set_longitudinal_personality(self, button_index: int):
self._params.put("LongitudinalPersonality", button_index)

View File

@@ -4,7 +4,8 @@ from dataclasses import dataclass
from collections.abc import Callable
from cereal import log
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
@@ -23,7 +24,6 @@ NetworkType = log.DeviceState.NetworkType
# Color scheme
class Colors:
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
WHITE = rl.WHITE
WHITE_DIM = rl.Color(255, 255, 255, 85)
GRAY = rl.Color(84, 84, 84, 255)
@@ -40,13 +40,13 @@ class Colors:
NETWORK_TYPES = {
NetworkType.none: "Offline",
NetworkType.wifi: "WiFi",
NetworkType.cell2G: "2G",
NetworkType.cell3G: "3G",
NetworkType.cell4G: "LTE",
NetworkType.cell5G: "5G",
NetworkType.ethernet: "Ethernet",
NetworkType.none: tr_noop("--"),
NetworkType.wifi: tr_noop("Wi-Fi"),
NetworkType.ethernet: tr_noop("ETH"),
NetworkType.cell2G: tr_noop("2G"),
NetworkType.cell3G: tr_noop("3G"),
NetworkType.cell4G: tr_noop("LTE"),
NetworkType.cell5G: tr_noop("5G"),
}
@@ -68,27 +68,33 @@ class Sidebar(Widget):
self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0
self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD)
self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD)
self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING)
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
self._recording_audio = False
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
self._mic_img = gui_app.texture("icons/microphone.png", 30, 30)
self._mic_indicator_rect = rl.Rectangle(0, 0, 0, 0)
self._font_regular = gui_app.font(FontWeight.NORMAL)
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
# Callbacks
self._on_settings_click: Callable | None = None
self._on_flag_click: Callable | None = None
self._open_settings_callback: Callable | None = None
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None):
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None,
open_settings: Callable | None = None):
self._on_settings_click = on_settings
self._on_flag_click = on_flag
self._open_settings_callback = open_settings
def _render(self, rect: rl.Rectangle):
# Background
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
rl.draw_rectangle_rec(rect, rl.BLACK)
self._draw_buttons(rect)
self._draw_network_indicator(rect)
@@ -101,13 +107,14 @@ class Sidebar(Widget):
device_state = sm['deviceState']
self._recording_audio = ui_state.recording_audio
self._update_network_status(device_state)
self._update_temperature_status(device_state)
self._update_connection_status(device_state)
self._update_panda_status()
def _update_network_status(self, device_state):
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown")
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
strength = device_state.networkStrength
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
@@ -115,26 +122,26 @@ class Sidebar(Widget):
thermal_status = device_state.thermalStatus
if thermal_status == ThermalStatus.green:
self._temp_status.update("TEMP", "GOOD", Colors.GOOD)
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
elif thermal_status == ThermalStatus.yellow:
self._temp_status.update("TEMP", "OK", Colors.WARNING)
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING)
else:
self._temp_status.update("TEMP", "HIGH", Colors.DANGER)
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)
def _update_connection_status(self, device_state):
last_ping = device_state.lastAthenaPingTime
if last_ping == 0:
self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING)
self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD)
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD)
else:
self._connect_status.update("CONNECT", "ERROR", Colors.DANGER)
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ERROR"), Colors.DANGER)
def _update_panda_status(self):
if ui_state.panda_type == log.PandaState.PandaType.unknown:
self._panda_status.update("NO", "PANDA", Colors.DANGER)
self._panda_status.update(tr_noop("NO"), tr_noop("PANDA"), Colors.DANGER)
else:
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
def _handle_mouse_release(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
@@ -143,6 +150,9 @@ class Sidebar(Widget):
elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started:
if self._on_flag_click:
self._on_flag_click()
elif self._recording_audio and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect):
if self._open_settings_callback:
self._open_settings_callback()
def _draw_buttons(self, rect: rl.Rectangle):
mouse_pos = rl.get_mouse_position()
@@ -160,6 +170,17 @@ class Sidebar(Widget):
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
# Microphone button
if self._recording_audio:
self._mic_indicator_rect = rl.Rectangle(rect.x + rect.width - 130, rect.y + 245, 75, 40)
mic_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect)
bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER
rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color)
rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE)
def _draw_network_indicator(self, rect: rl.Rectangle):
# Signal strength dots
x_start = rect.x + 58
@@ -176,7 +197,7 @@ class Sidebar(Widget):
# Network type text
text_y = rect.y + 247
text_pos = rl.Vector2(rect.x + 58, text_y)
rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE)
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
def _draw_metrics(self, rect: rl.Rectangle):
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
@@ -189,15 +210,15 @@ class Sidebar(Widget):
# Draw colored left edge (clipped rounded rectangle)
edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118)
rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height))
rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color)
rl.draw_rectangle_rounded(edge_rect, 0.3, 10, metric.color)
rl.end_scissor_mode()
# Draw border
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER)
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER)
# Draw label and value
labels = [metric.label, metric.value]
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE)
labels = [tr(metric.label), tr(metric.value)]
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
for text in labels:
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
text_y += text_size.y

View File

@@ -1,12 +1,16 @@
import time
from functools import lru_cache
from openpilot.common.api import Api
from openpilot.common.time_helpers import system_time_valid
TOKEN_EXPIRY_HOURS = 2
@lru_cache(maxsize=1)
def _get_token(dongle_id: str, t: int):
if not system_time_valid():
raise RuntimeError("System time is not valid, cannot generate token")
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)

View File

@@ -11,14 +11,14 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token
class PrimeType(IntEnum):
UNKNOWN = -2,
UNPAIRED = -1,
NONE = 0,
MAGENTA = 1,
LITE = 2,
BLUE = 3,
MAGENTA_NEW = 4,
PURPLE = 5,
UNKNOWN = -2
UNPAIRED = -1
NONE = 0
MAGENTA = 1
LITE = 2
BLUE = 3
MAGENTA_NEW = 4
PURPLE = 5
class PrimeState:
@@ -33,7 +33,6 @@ class PrimeState:
self._running = False
self._thread = None
self.start()
def _load_initial_state(self) -> PrimeType:
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType")
@@ -96,5 +95,9 @@ class PrimeState:
with self._lock:
return bool(self.prime_type > PrimeType.NONE)
def is_paired(self) -> bool:
with self._lock:
return self.prime_type > PrimeType.UNPAIRED
def __del__(self):
self.stop()

View File

@@ -1,36 +0,0 @@
#include <sys/resource.h>
#include <QApplication>
#include <QTranslator>
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/window.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/qt/window.h"
#define MainWindow MainWindowSP
#else
#include "selfdrive/ui/qt/qt_window.h"
#endif
int main(int argc, char *argv[]) {
setpriority(PRIO_PROCESS, 0, -20);
qInstallMessageHandler(swagLogMessageHandler);
initApp(argc, argv);
QTranslator translator;
QString translation_file = QString::fromStdString(Params().get("LanguageSetting"));
if (!translator.load(QString(":/%1").arg(translation_file)) && translation_file.length()) {
qCritical() << "Failed to load translation file:" << translation_file;
}
QApplication a(argc, argv);
a.installTranslator(&translator);
MainWindow w;
setMainWindow(&w);
a.installEventFilter(&w);
return a.exec();
}

View File

@@ -4,10 +4,11 @@ from dataclasses import dataclass
from cereal import messaging, log
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS
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
from openpilot.system.ui.widgets.label import gui_text_box
from openpilot.system.ui.widgets.label import Label
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
@@ -21,14 +22,19 @@ ALERT_FONT_SMALL = 66
ALERT_FONT_MEDIUM = 74
ALERT_FONT_BIG = 88
ALERT_HEIGHTS = {
AlertSize.small: 271,
AlertSize.mid: 420,
}
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
# Constants
ALERT_COLORS = {
AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red
AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1
AlertStatus.userPrompt: rl.Color(0xDA, 0x6F, 0x25, 0xF1), # #DA6F25 with alpha 0xF1
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1
}
@@ -42,24 +48,24 @@ class Alert:
# Pre-defined alert instances
ALERT_STARTUP_PENDING = Alert(
text1="openpilot Unavailable",
text2="Waiting to start",
text1=tr("openpilot Unavailable"),
text2=tr("Waiting to start"),
size=AlertSize.mid,
status=AlertStatus.normal,
)
ALERT_CRITICAL_TIMEOUT = Alert(
text1="TAKE CONTROL IMMEDIATELY",
text2="System Unresponsive",
text1=tr("TAKE CONTROL IMMEDIATELY"),
text2=tr("System Unresponsive"),
size=AlertSize.full,
status=AlertStatus.critical,
)
ALERT_CRITICAL_REBOOT = Alert(
text1="System Unresponsive",
text2="Reboot Device",
size=AlertSize.full,
status=AlertStatus.critical,
text1=tr("System Unresponsive"),
text2=tr("Reboot Device"),
size=AlertSize.mid,
status=AlertStatus.normal,
)
@@ -69,14 +75,20 @@ class AlertRenderer(Widget):
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
# font size is set dynamically
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
"""Generate the current alert based on selfdrive state."""
ss = sm['selfdriveState']
# Check if selfdriveState messages have stopped arriving
recv_frame = sm.recv_frame['selfdriveState']
if not sm.updated['selfdriveState']:
recv_frame = sm.recv_frame['selfdriveState']
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
time_since_onroad = time.monotonic() - ui_state.started_time
# 1. Never received selfdriveState since going onroad
waiting_for_startup = recv_frame < ui_state.started_frame
@@ -95,13 +107,17 @@ class AlertRenderer(Widget):
if ss.alertSize == 0:
return None
# Don't get old alert
if recv_frame < ui_state.started_frame:
return None
# Return current alert
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw)
def _render(self, rect: rl.Rectangle) -> bool:
def _render(self, rect: rl.Rectangle):
alert = self.get_alert(ui_state.sm)
if not alert:
return False
return
alert_rect = self._get_alert_rect(rect, alert.size)
self._draw_background(alert_rect, alert)
@@ -113,21 +129,14 @@ class AlertRenderer(Widget):
alert_rect.height - 2 * ALERT_PADDING
)
self._draw_text(text_rect, alert)
return True
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
if size == AlertSize.full:
return rect
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == AlertSize.small else
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
return rl.Rectangle(
rect.x + ALERT_MARGIN,
rect.y + rect.height - ALERT_MARGIN - height,
rect.width - 2 * ALERT_MARGIN,
height
)
h = ALERT_HEIGHTS.get(size, rect.height)
return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN,
rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2)
def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None:
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
@@ -150,13 +159,17 @@ class AlertRenderer(Widget):
else:
is_long = len(alert.text1) > 15
font_size1 = 132 if is_long else 177
align_ment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER
vertical_align = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE
text_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height // 2)
gui_text_box(text_rect, alert.text1, font_size1, alignment=align_ment, alignment_vertical=vertical_align, font_weight=FontWeight.BOLD)
text_rect.y = rect.y + rect.height // 2
gui_text_box(text_rect, alert.text2, ALERT_FONT_BIG, alignment=align_ment)
top_offset = 200 if is_long or '\n' in alert.text1 else 270
title_rect = rl.Rectangle(rect.x, rect.y + top_offset, rect.width, 600)
self._full_text1_label.set_font_size(font_size1)
self._full_text1_label.set_text(alert.text1)
self._full_text1_label.render(title_rect)
bottom_offset = 361 if is_long else 420
subtitle_rect = rl.Rectangle(rect.x, rect.y + rect.height - bottom_offset, rect.width, 300)
self._full_text2_label.set_text(alert.text2)
self._full_text2_label.render(subtitle_rect)
def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None:
text_size = measure_text_cached(font, text, font_size)

View File

@@ -1,9 +1,10 @@
import time
import numpy as np
import pyray as rl
from collections.abc import Callable
from cereal import log
from cereal import log, messaging
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
from openpilot.selfdrive.ui import UI_BORDER_SIZE
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
@@ -20,13 +21,14 @@ WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
BORDER_COLORS = {
UIStatus.DISENGAGED: rl.Color(0x17, 0x33, 0x49, 0xC8), # Blue for disengaged state
UIStatus.OVERRIDE: rl.Color(0x91, 0x9B, 0x95, 0xF1), # Gray for override state
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state
}
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
INF_POINT = np.array([1000.0, 0.0, 0.0])
class AugmentedRoadView(CameraView):
@@ -38,9 +40,7 @@ class AugmentedRoadView(CameraView):
self.view_from_calib = view_frame_from_device_frame.copy()
self.view_from_wide_calib = view_frame_from_device_frame.copy()
self._last_calib_time: float = 0
self._last_rect_dims = (0.0, 0.0)
self._last_stream_type = stream_type
self._matrix_cache_key = (0, 0.0, 0.0, stream_type)
self._cached_matrix: np.ndarray | None = None
self._content_rect = rl.Rectangle()
@@ -49,14 +49,12 @@ class AugmentedRoadView(CameraView):
self.alert_renderer = AlertRenderer()
self.driver_state_renderer = DriverStateRenderer()
# Callbacks
self._click_callback: Callable | None = None
def set_callbacks(self, on_click: Callable | None = None):
self._click_callback = on_click
# debug
self._pm = messaging.PubMaster(['uiDebug'])
def _render(self, rect):
# Only render when system is started to avoid invalid data access
start_draw = time.monotonic()
if not ui_state.started:
return
@@ -73,9 +71,6 @@ class AugmentedRoadView(CameraView):
rect.height - 2 * UI_BORDER_SIZE,
)
# Draw colored border based on driving state
self._draw_border(rect)
# Enable scissor mode to clip all rendering within content rectangle boundaries
# This creates a rendering viewport that prevents graphics from drawing outside the border
rl.begin_scissor_mode(
@@ -91,8 +86,8 @@ class AugmentedRoadView(CameraView):
# Draw all UI overlays
self.model_renderer.render(self._content_rect)
self._hud_renderer.render(self._content_rect)
if not self.alert_renderer.render(self._content_rect):
self.driver_state_renderer.render(self._content_rect)
self.alert_renderer.render(self._content_rect)
self.driver_state_renderer.render(self._content_rect)
# Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds
@@ -100,15 +95,29 @@ class AugmentedRoadView(CameraView):
# End clipping region
rl.end_scissor_mode()
# Handle click events if no HUD interaction occurred
if not self._hud_renderer.handle_mouse_event():
if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
self._click_callback()
# Draw colored border based on driving state
self._draw_border(rect)
# publish uiDebug
msg = messaging.new_message('uiDebug')
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
self._pm.send('uiDebug', msg)
def _handle_mouse_press(self, _):
if not self._hud_renderer.user_interacting() and self._click_callback is not None:
self._click_callback()
def _handle_mouse_release(self, _):
# We only call click callback on press if not interacting with HUD
pass
def _draw_border(self, rect: rl.Rectangle):
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
border_roundness = 0.12
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color)
def _switch_stream_if_needed(self, sm):
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
@@ -151,12 +160,13 @@ class AugmentedRoadView(CameraView):
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
# Check if we can use cached matrix
calib_time = ui_state.sm.recv_frame['liveCalibration']
current_dims = (self._content_rect.width, self._content_rect.height)
if (self._last_calib_time == calib_time and
self._last_rect_dims == current_dims and
self._last_stream_type == self.stream_type and
self._cached_matrix is not None):
cache_key = (
ui_state.sm.recv_frame['liveCalibration'],
self._content_rect.width,
self._content_rect.height,
self.stream_type
)
if cache_key == self._matrix_cache_key and self._cached_matrix is not None:
return self._cached_matrix
# Get camera configuration
@@ -167,9 +177,8 @@ class AugmentedRoadView(CameraView):
zoom = 2.0 if is_wide_camera else 1.1
# Calculate transforms for vanishing point
inf_point = np.array([1000.0, 0.0, 0.0])
calib_transform = intrinsic @ calibration
kep = calib_transform @ inf_point
kep = calib_transform @ INF_POINT
# Calculate center points and dimensions
x, y = self._content_rect.x, self._content_rect.y
@@ -192,9 +201,7 @@ class AugmentedRoadView(CameraView):
x_offset, y_offset = 0, 0
# Cache the computed transformation matrix to avoid recalculations
self._last_calib_time = calib_time
self._last_rect_dims = current_dims
self._last_stream_type = self.stream_type
self._matrix_cache_key = cache_key
self._cached_matrix = np.array([
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
[0, zoom * 2 * cy / h, -y_offset / h * 2],

View File

@@ -8,6 +8,7 @@ from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
@@ -67,6 +68,7 @@ else:
class CameraView(Widget):
def __init__(self, name: str, stream_type: VisionStreamType):
super().__init__()
# TODO: implement a receiver and connect thread
self._name = name
# Primary stream
self.client = VisionIpcClient(name, stream_type, conflate=True)
@@ -103,6 +105,20 @@ class CameraView(Widget):
self.egl_texture = rl.load_texture_from_image(temp_image)
rl.unload_image(temp_image)
ui_state.add_offroad_transition_callback(self._offroad_transition)
def _offroad_transition(self):
# Reconnect if not first time going onroad
if ui_state.is_onroad() and self.frame is not None:
# Prevent old frames from showing when going onroad. Qt has a separate thread
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
# and only clears internal buffers, not the message queue.
self.frame = None
self.available_streams.clear()
if self.client:
del self.client
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
def _set_placeholder_color(self, color: rl.Color):
"""Set a placeholder color to be drawn when no frame is available."""
self._placeholder_color = color
@@ -139,6 +155,8 @@ class CameraView(Widget):
if self.shader and self.shader.id:
rl.unload_shader(self.shader)
self.frame = None
self.available_streams.clear()
self.client = None
def __del__(self):
@@ -175,6 +193,9 @@ class CameraView(Widget):
if buffer:
self._texture_needs_update = True
self.frame = buffer
elif not self.client.is_connected():
# ensure we clear the displayed frame when the connection is lost
self.frame = None
if not self.frame:
self._draw_placeholder(rect)

View File

@@ -3,8 +3,9 @@ import pyray as rl
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.label import gui_label
@@ -12,17 +13,25 @@ class DriverCameraDialog(CameraView):
def __init__(self):
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
self.driver_state_renderer = DriverStateRenderer()
# TODO: this can grow unbounded, should be given some thought
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
ui_state.params.put_bool("IsDriverViewEnabled", True)
def stop_dmonitoringmodeld(self):
ui_state.params.put_bool("IsDriverViewEnabled", False)
gui_app.set_modal_overlay(None)
def _handle_mouse_release(self, _):
super()._handle_mouse_release(_)
self.stop_dmonitoringmodeld()
def _render(self, rect):
super()._render(rect)
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
return 1
if not self.frame:
gui_label(
rect,
"camera starting",
tr("camera starting"),
font_size=100,
font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,

View File

@@ -1,10 +1,14 @@
import numpy as np
import pyray as rl
from cereal import log
from dataclasses import dataclass
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
from openpilot.selfdrive.ui import UI_BORDER_SIZE
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
AlertSize = log.SelfdriveState.AlertSize
# Default 3D coordinates for face keypoints as a NumPy array
DEFAULT_FACE_KPTS_3D = np.array([
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
@@ -50,7 +54,6 @@ class DriverStateRenderer(Widget):
self.is_active = False
self.is_rhd = False
self.dm_fade_state = 0.0
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
self.driver_pose_sins = np.zeros(3, dtype=np.float32)
@@ -75,8 +78,8 @@ class DriverStateRenderer(Widget):
self.engaged_color = rl.Color(26, 242, 66, 255)
self.disengaged_color = rl.Color(139, 139, 139, 255)
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
ui_state.sm.seen['driverMonitoringState']))
self.set_visible(lambda: (ui_state.sm["selfdriveState"].alertSize == AlertSize.none and
ui_state.sm.recv_frame["driverStateV2"] > ui_state.started_frame))
def _render(self, rect):
# Set opacity based on active state
@@ -106,11 +109,7 @@ class DriverStateRenderer(Widget):
def _update_state(self):
"""Update the driver monitoring state based on model data"""
sm = ui_state.sm
if not sm.updated["driverMonitoringState"]:
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height):
self._pre_calculate_drawing_elements()
self.last_rect = self._rect
if not self.is_visible:
return
# Get monitoring state
@@ -222,7 +221,7 @@ class DriverStateRenderer(Widget):
radius_y = arc_data.height / 2
x_coords = center_x + np.cos(angles) * radius_x
y_coords = center_y + np.sin(angles) * radius_y
y_coords = center_y - np.sin(angles) * radius_y
arc_lines = self.h_arc_lines if is_horizontal else self.v_arc_lines
for i, (x_coord, y_coord) in enumerate(zip(x_coords, y_coords, strict=True)):

View File

@@ -32,26 +32,21 @@ class ExpButton(Widget):
self._experimental_mode = selfdrive_state.experimentalMode
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
def handle_mouse_event(self) -> bool:
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and
self._is_toggle_allowed()):
new_mode = not self._experimental_mode
self._params.put_bool("ExperimentalMode", new_mode)
def _handle_mouse_release(self, _):
super()._handle_mouse_release(_)
if self._is_toggle_allowed():
new_mode = not self._experimental_mode
self._params.put_bool("ExperimentalMode", new_mode)
# Hold new state temporarily
self._held_mode = new_mode
self._hold_end_time = time.monotonic() + self._hold_duration
return True
return False
# Hold new state temporarily
self._held_mode = new_mode
self._hold_end_time = time.monotonic() + self._hold_duration
def _render(self, rect: rl.Rectangle) -> None:
center_x = int(self._rect.x + self._rect.width // 2)
center_y = int(self._rect.y + self._rect.height // 2)
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
self._white_color.a = 180 if self.is_pressed or not self._engageable else 255
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
@@ -71,8 +66,5 @@ class ExpButton(Widget):
if not self._params.get_bool("ExperimentalModeConfirmed"):
return False
car_params = ui_state.sm["carParams"]
if car_params.alphaLongitudinalAvailable:
return self._params.get_bool("AlphaLongitudinalEnabled")
else:
return car_params.openpilotLongitudinalControl
# Mirror exp mode toggle using persistent car params
return ui_state.has_longitudinal_control

View File

@@ -4,6 +4,7 @@ from openpilot.common.constants import CV
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
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
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
@@ -60,7 +61,7 @@ class HudRenderer(Widget):
super().__init__()
"""Initialize the HUD renderer."""
self.is_cruise_set: bool = False
self.is_cruise_available: bool = False
self.is_cruise_available: bool = True
self.set_speed: float = SET_SPEED_NA
self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False
@@ -69,7 +70,7 @@ class HudRenderer(Widget):
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
self._exp_button: ExpButton = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
def _update_state(self) -> None:
"""Update HUD state based on car state and controls state."""
@@ -120,8 +121,8 @@ class HudRenderer(Widget):
button_y = rect.y + UI_CONFIG.border_size
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
def handle_mouse_event(self) -> bool:
return bool(self._exp_button.handle_mouse_event())
def user_interacting(self) -> bool:
return self._exp_button.is_pressed
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
"""Draw the MAX speed indicator box."""
@@ -130,8 +131,8 @@ class HudRenderer(Widget):
y = rect.y + 45
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
rl.draw_rectangle_rounded(set_speed_rect, 0.2, 30, COLORS.black_translucent)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.2, 30, 6, COLORS.border_translucent)
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.black_translucent)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.border_translucent)
max_color = COLORS.grey
set_speed_color = COLORS.dark_grey
@@ -144,7 +145,7 @@ class HudRenderer(Widget):
elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.override
max_text = "MAX"
max_text = tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
rl.draw_text_ex(
self._font_semi_bold,
@@ -173,7 +174,7 @@ class HudRenderer(Widget):
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
unit_text = "km/h" if ui_state.is_metric else "mph"
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)

View File

@@ -3,20 +3,17 @@ import numpy as np
import pyray as rl
from cereal import messaging, car
from dataclasses import dataclass, field
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import DEFAULT_FPS
from openpilot.system.ui.lib.shader_polygon import draw_polygon
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from openpilot.system.ui.widgets import Widget
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
MAX_DRAW_DISTANCE = 100.0
PATH_COLOR_TRANSITION_DURATION = 0.5 # Seconds for color transition animation
PATH_BLEND_INCREMENT = 1.0 / (PATH_COLOR_TRANSITION_DURATION * DEFAULT_FPS)
MAX_POINTS = 200
THROTTLE_COLORS = [
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
@@ -49,7 +46,7 @@ class ModelRenderer(Widget):
super().__init__()
self._longitudinal_control = False
self._experimental_mode = False
self._blend_factor = 1.0
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
self._prev_allow_throttle = True
self._lane_line_probs = np.zeros(4, dtype=np.float32)
self._road_edge_stds = np.zeros(2, dtype=np.float32)
@@ -67,12 +64,12 @@ class ModelRenderer(Widget):
self._transform_dirty = True
self._clip_region = None
self._exp_gradient = {
'start': (0.0, 1.0), # Bottom of path
'end': (0.0, 0.0), # Top of path
'colors': [],
'stops': [],
}
self._exp_gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
colors=[],
stops=[],
)
# Get longitudinal control setting from car parameters
if car_params := Params().get("CarParams"):
@@ -170,12 +167,12 @@ class ModelRenderer(Widget):
# Update lane lines using raw points
for i, lane_line in enumerate(self._lane_lines):
lane_line.projected_points = self._map_line_to_polygon(
lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx
lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx, max_distance
)
# Update road edges using raw points
for road_edge in self._road_edges:
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx)
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
# Update path using raw points
if lead and lead.status:
@@ -184,7 +181,7 @@ class ModelRenderer(Widget):
max_idx = self._get_path_length_idx(path_x_array, max_distance)
self._path.projected_points = self._map_line_to_polygon(
self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False
self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False
)
self._update_experimental_gradient()
@@ -227,8 +224,12 @@ class ModelRenderer(Widget):
i += 1 + (1 if (i + 2) < max_len else 0)
# Store the gradient in the path object
self._exp_gradient['colors'] = segment_colors
self._exp_gradient['stops'] = gradient_stops
self._exp_gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
colors=segment_colors,
stops=gradient_stops,
)
def _update_lead_vehicle(self, d_rel, v_rel, point, rect):
speed_buff, lead_buff = 10.0, 40.0
@@ -277,36 +278,25 @@ class ModelRenderer(Widget):
if not self._path.projected_points.size:
return
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
self._blend_filter.update(int(allow_throttle))
if self._experimental_mode:
# Draw with acceleration coloring
if len(self._exp_gradient['colors']) > 1:
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
else:
# Draw with throttle/no throttle gradient
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
# Start transition if throttle state changes
if allow_throttle != self._prev_allow_throttle:
self._prev_allow_throttle = allow_throttle
self._blend_factor = max(1.0 - self._blend_factor, 0.0)
# Update blend factor
if self._blend_factor < 1.0:
self._blend_factor = min(self._blend_factor + PATH_BLEND_INCREMENT, 1.0)
begin_colors = NO_THROTTLE_COLORS if allow_throttle else THROTTLE_COLORS
end_colors = THROTTLE_COLORS if allow_throttle else NO_THROTTLE_COLORS
# Blend colors based on transition
blended_colors = self._blend_colors(begin_colors, end_colors, self._blend_factor)
gradient = {
'start': (0.0, 1.0), # Bottom of path
'end': (0.0, 0.0), # Top of path
'colors': blended_colors,
'stops': [0.0, 0.5, 1.0],
}
# Blend throttle/no throttle colors based on transition
blend_factor = round(self._blend_filter.x * 100) / 100
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
colors=blended_colors,
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
def _draw_lead_indicator(self):
@@ -319,11 +309,11 @@ class ModelRenderer(Widget):
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha))
@staticmethod
def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int:
"""Get the index corresponding to the given path height"""
def _get_path_length_idx(pos_x_array: np.ndarray, path_distance: float) -> int:
"""Get the index corresponding to the given path distance"""
if len(pos_x_array) == 0:
return 0
indices = np.where(pos_x_array <= path_height)[0]
indices = np.where(pos_x_array <= path_distance)[0]
return indices[-1] if indices.size > 0 else 0
def _map_to_screen(self, in_x, in_y, in_z):
@@ -342,13 +332,24 @@ class ModelRenderer(Widget):
return (x, y)
def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, allow_invert: bool = True) -> np.ndarray:
def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, max_distance: float, allow_invert: bool = True) -> np.ndarray:
"""Convert 3D line to 2D polygon for rendering."""
if line.shape[0] == 0:
return np.empty((0, 2), dtype=np.float32)
# Slice points and filter non-negative x-coordinates
points = line[:max_idx + 1]
# Interpolate around max_idx so path end is smooth (max_distance is always >= p0.x)
if 0 < max_idx < line.shape[0] - 1:
p0 = line[max_idx]
p1 = line[max_idx + 1]
x0, x1 = p0[0], p1[0]
interp_y = np.interp(max_distance, [x0, x1], [p0[1], p1[1]])
interp_z = np.interp(max_distance, [x0, x1], [p0[2], p1[2]])
interp_point = np.array([max_distance, interp_y, interp_z], dtype=points.dtype)
points = np.concatenate((points, interp_point[None, :]), axis=0)
points = points[points[:, 0] >= 0]
if points.shape[0] == 0:
return np.empty((0, 2), dtype=np.float32)

View File

@@ -1,161 +0,0 @@
#include "selfdrive/ui/qt/body.h"
#include <cmath>
#include <algorithm>
#include <QPainter>
#include <QStackedLayout>
#include "common/params.h"
#include "common/timing.h"
RecordButton::RecordButton(QWidget *parent) : QPushButton(parent) {
setCheckable(true);
setChecked(false);
setFixedSize(148, 148);
QObject::connect(this, &QPushButton::toggled, [=]() {
setEnabled(false);
});
}
void RecordButton::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
QPoint center(width() / 2, height() / 2);
QColor bg(isChecked() ? "#FFFFFF" : "#737373");
QColor accent(isChecked() ? "#FF0000" : "#FFFFFF");
if (!isEnabled()) {
bg = QColor("#404040");
accent = QColor("#FFFFFF");
}
if (isDown()) {
accent.setAlphaF(0.7);
}
p.setPen(Qt::NoPen);
p.setBrush(bg);
p.drawEllipse(center, 74, 74);
p.setPen(QPen(accent, 6));
p.setBrush(Qt::NoBrush);
p.drawEllipse(center, 42, 42);
p.setPen(Qt::NoPen);
p.setBrush(accent);
p.drawEllipse(center, 22, 22);
}
BodyWindow::BodyWindow(QWidget *parent) : fuel_filter(1.0, 5., 1. / UI_FREQ), QWidget(parent) {
QStackedLayout *layout = new QStackedLayout(this);
layout->setStackingMode(QStackedLayout::StackAll);
QWidget *w = new QWidget;
QVBoxLayout *vlayout = new QVBoxLayout(w);
vlayout->setMargin(45);
layout->addWidget(w);
// face
face = new QLabel();
face->setAlignment(Qt::AlignCenter);
layout->addWidget(face);
awake = new QMovie("../assets/body/awake.gif", {}, this);
awake->setCacheMode(QMovie::CacheAll);
sleep = new QMovie("../assets/body/sleep.gif", {}, this);
sleep->setCacheMode(QMovie::CacheAll);
// record button
btn = new RecordButton(this);
vlayout->addWidget(btn, 0, Qt::AlignBottom | Qt::AlignRight);
QObject::connect(btn, &QPushButton::clicked, [=](bool checked) {
btn->setEnabled(false);
Params().putBool("DisableLogging", !checked);
last_button = nanos_since_boot();
});
w->raise();
QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState);
}
void BodyWindow::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.fillRect(rect(), QColor(0, 0, 0));
// battery outline + detail
p.translate(width() - 136, 16);
const QColor gray = QColor("#737373");
p.setBrush(Qt::NoBrush);
p.setPen(QPen(gray, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
p.drawRoundedRect(2, 2, 78, 36, 8, 8);
p.setPen(Qt::NoPen);
p.setBrush(gray);
p.drawRoundedRect(84, 12, 6, 16, 4, 4);
p.drawRect(84, 12, 3, 16);
// battery level
double fuel = std::clamp(fuel_filter.x(), 0.2f, 1.0f);
const int m = 5; // manual margin since we can't do an inner border
p.setPen(Qt::NoPen);
p.setBrush(fuel > 0.25 ? QColor("#32D74B") : QColor("#FF453A"));
p.drawRoundedRect(2 + m, 2 + m, (78 - 2*m)*fuel, 36 - 2*m, 4, 4);
// charging status
if (charging) {
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
const QPolygonF charger({
QPointF(12.31, 0),
QPointF(12.31, 16.92),
QPointF(18.46, 16.92),
QPointF(6.15, 40),
QPointF(6.15, 23.08),
QPointF(0, 23.08),
});
p.drawPolygon(charger.translated(98, 0));
}
}
void BodyWindow::offroadTransition(bool offroad) {
btn->setChecked(true);
btn->setEnabled(true);
fuel_filter.reset(1.0);
}
void BodyWindow::updateState(const UIState &s) {
if (!isVisible()) {
return;
}
const SubMaster &sm = *(s.sm);
auto cs = sm["carState"].getCarState();
charging = cs.getCharging();
fuel_filter.update(cs.getFuelGauge());
// TODO: use carState.standstill when that's fixed
const bool standstill = std::abs(cs.getVEgo()) < 0.01;
QMovie *m = standstill ? sleep : awake;
if (m != face->movie()) {
face->setMovie(m);
face->movie()->start();
}
// update record button state
if (sm.updated("managerState") && (sm.rcv_time("managerState") - last_button)*1e-9 > 0.5) {
for (auto proc : sm["managerState"].getManagerState().getProcesses()) {
if (proc.getName() == "loggerd") {
btn->setEnabled(true);
btn->setChecked(proc.getRunning());
}
}
}
update();
}

View File

@@ -1,44 +0,0 @@
#pragma once
#include <QMovie>
#include <QLabel>
#include <QPushButton>
#include "common/util.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/ui.h"
#define UIState UIStateSP
#else
#include "selfdrive/ui/ui.h"
#endif
class RecordButton : public QPushButton {
Q_OBJECT
public:
RecordButton(QWidget* parent = 0);
private:
void paintEvent(QPaintEvent*) override;
};
class BodyWindow : public QWidget {
Q_OBJECT
public:
BodyWindow(QWidget* parent = 0);
private:
bool charging = false;
uint64_t last_button = 0;
FirstOrderFilter fuel_filter;
QLabel *face;
QMovie *awake, *sleep;
RecordButton *btn;
void paintEvent(QPaintEvent*) override;
private slots:
void updateState(const UIState &s);
void offroadTransition(bool onroad);
};

View File

@@ -1,95 +0,0 @@
#include "selfdrive/ui/qt/home.h"
#include <QHBoxLayout>
#include <QMouseEvent>
#include "selfdrive/ui/qt/util.h"
// HomeWindow: the container for the offroad and onroad UIs
HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
QHBoxLayout *main_layout = new QHBoxLayout(this);
main_layout->setMargin(0);
main_layout->setSpacing(0);
sidebar = new Sidebar(this);
main_layout->addWidget(sidebar);
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings);
slayout = new QStackedLayout();
main_layout->addLayout(slayout);
home = new OffroadHome(this);
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings);
slayout->addWidget(home);
onroad = new OnroadWindow(this);
slayout->addWidget(onroad);
body = new BodyWindow(this);
slayout->addWidget(body);
driver_view = new DriverViewWindow(this);
connect(driver_view, &DriverViewWindow::done, [=] {
showDriverView(false);
});
slayout->addWidget(driver_view);
setAttribute(Qt::WA_NoSystemBackground);
QObject::connect(uiState(), &UIState::uiUpdate, this, &HomeWindow::updateState);
QObject::connect(uiState(), &UIState::offroadTransition, this, &HomeWindow::offroadTransition);
QObject::connect(uiState(), &UIState::offroadTransition, sidebar, &Sidebar::offroadTransition);
}
void HomeWindow::showSidebar(bool show) {
sidebar->setVisible(show);
}
void HomeWindow::updateState(const UIState &s) {
const SubMaster &sm = *(s.sm);
// switch to the generic robot UI
if (onroad->isVisible() && !body->isEnabled() && sm["carParams"].getCarParams().getNotCar()) {
body->setEnabled(true);
slayout->setCurrentWidget(body);
}
}
void HomeWindow::offroadTransition(bool offroad) {
body->setEnabled(false);
sidebar->setVisible(offroad);
if (offroad) {
slayout->setCurrentWidget(home);
} else {
slayout->setCurrentWidget(onroad);
}
}
void HomeWindow::showDriverView(bool show) {
if (show) {
emit closeSettings();
slayout->setCurrentWidget(driver_view);
} else {
slayout->setCurrentWidget(home);
}
sidebar->setVisible(show == false);
}
void HomeWindow::mousePressEvent(QMouseEvent* e) {
// Handle sidebar collapsing
if ((onroad->isVisible() || body->isVisible()) && (!sidebar->isVisible() || e->x() > sidebar->width())) {
sidebar->setVisible(!sidebar->isVisible());
}
}
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
HomeWindow::mousePressEvent(e);
const SubMaster &sm = *(uiState()->sm);
if (sm["carParams"].getCarParams().getNotCar()) {
if (onroad->isVisible()) {
slayout->setCurrentWidget(body);
} else if (body->isVisible()) {
slayout->setCurrentWidget(onroad);
}
showSidebar(false);
}
}

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