Compare commits

...

115 Commits

Author SHA1 Message Date
rav4kumar
dd514a5d56 sunnylink desc 2025-12-14 11:11:59 -07:00
rav4kumar
4ecd5f4f99 Add AccelPersonality and AccelPersonalityEnabled to params metadata 2025-12-14 10:56:46 -07:00
rav4kumar
d6a81d0b30 Add support for dynamic acceleration personalities 2025-12-14 10:45:04 -07:00
Jason Wen
d9bbc8f5bb ci: update prebuilt exclusions (#1572) 2025-12-13 15:17:20 -05:00
Jason Wen
a33e01ca96 Sync: commaai/openpilot:mastersunnypilot/openpilot:master (#1571) 2025-12-13 01:58:07 -05:00
Jason Wen
03c8494dbc Merge branch 'upstream/openpilot/master' into sync-20251213
# Conflicts:
#	README.md
#	common/api.py
#	docs/CARS.md
#	opendbc_repo
#	panda
#	selfdrive/ui/mici/layouts/offroad_alerts.py
#	system/ui/README.md
#	system/version.py
2025-12-13 01:50:42 -05:00
James Vecellio-Grant
cab2a28e10 ui: Developer panel extension (#1521)
* ui: developer panel

* comment out

* double translate

* quickboot and more efficient file checking

* use HtmlModalSP!

* ui: `HtmlModalSP`

* less

* lint

* less

* just use existing dir on PC

* grammar

* match official

* rename

* biiig

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-13 00:50:57 -05:00
Shane Smiskol
cc119b2a37 comma four: adjust Wifi scroller sizes (#36854)
* adjust sizes

* back
2025-12-12 21:37:26 -08:00
Harald Schäfer
9421e1cbfe Dark Souls 2 (#36849)
4b78e2e6-660f-4155-9105-81d4d8c658cd/400
2025-12-12 18:04:16 -08:00
Jason Wen
46257aca02 ui: HtmlModalSP (#1570)
* ui: `HtmlModalSP`

* less
2025-12-12 03:16:57 -05:00
James Vecellio-Grant
436ff5aa42 ui: OSM panel (#1515)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* sp raylib preview

* fix callback

* fix ui preview

* dialog txt

* compare vs what used to be done before InputDialog

* merge origin raylib toggles

* tree dialog

* less trees for the planet

* the heck

* save the trees we got icons

* Update process.py

* Remove 'sunnypilot_ui' 

Removed 'sunnypilot_ui' parameter from params_keys.h

* Update raylib_screenshots.py

Removed the parameter setting for 'sunnypilot_ui' in the test.

* ui: fuzzy search helper

* better tree. fully dynamic and stuff

* rm

* more indent

* Squashed commit of the following:

commit 6b5b686fa5
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 24 17:16:17 2025 -0800

    more indent

commit 76bc538ac7
Merge: 53eb821dc4 c53e2134e2
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Mon Nov 24 17:15:48 2025 -0800

    Merge branch 'master' into rl-tree-dialog

commit 53eb821dc4
Merge: 82e1ebe97e 844f4cbc74
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Mon Nov 24 11:54:55 2025 -0800

    Merge branch 'master' into rl-tree-dialog

commit 82e1ebe97e
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 24 10:23:35 2025 -0800

    rm

commit da3ff45bb6
Merge: 41da513fca a829a1b972
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 24 10:19:08 2025 -0800

    Merge remote-tracking branch 'origin/rl-tree-dialog' into rl-tree-dialog

commit 41da513fca
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 24 10:18:43 2025 -0800

    better tree. fully dynamic and stuff

commit b2950149fb
Merge: 4fb8e4beed 924e5a3211
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 24 10:17:51 2025 -0800

    Merge remote-tracking branch 'origin/input-dialog' into rl-tree-dialog

commit a829a1b972
Merge: 848290d07e 9edc36ca66
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Mon Nov 24 10:16:28 2025 -0800

    Merge branch 'master' into rl-tree-dialog

commit 4fb8e4beed
Merge: 848290d07e af4f0f8372
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 24 10:16:20 2025 -0800

    Merge remote-tracking branch 'origin/fuzzy-dialog' into rl-tree-dialog

commit af4f0f8372
Merge: 1d5f0ab282 3cd55260d9
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Mon Nov 24 06:39:29 2025 -0800

    Merge branch 'master' into fuzzy-dialog

commit 1d5f0ab282
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Sun Nov 23 11:28:59 2025 -0800

    ui: fuzzy search helper

commit 848290d07e
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 21:08:07 2025 -0800

    Update raylib_screenshots.py

    Removed the parameter setting for 'sunnypilot_ui' in the test.

commit 6694928a46
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 21:06:57 2025 -0800

    Remove 'sunnypilot_ui'

    Removed 'sunnypilot_ui' parameter from params_keys.h

commit b3c90ef7b2
Merge: 0d3bc959c8 457b6634fd
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 21:06:04 2025 -0800

    Merge branch 'master' into rl-tree-dialog

commit 924e5a3211
Merge: a4ee4ba76d d92d2cb683
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 19:33:18 2025 -0800

    Merge branch 'master' into input-dialog

commit a4ee4ba76d
Merge: e911de5968 4f13a0f775
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 16:24:31 2025 -0800

    Merge branch 'master' into input-dialog

commit e911de5968
Merge: cea6e00819 0ba5cbea91
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 13:50:33 2025 -0800

    Merge branch 'master' into input-dialog

commit cea6e00819
Merge: d7b8ce86ed 8184cd8a6a
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 12:01:45 2025 -0800

    Merge branch 'master' into input-dialog

commit 0d3bc959c8
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Wed Nov 19 20:29:30 2025 -0800

    Update process.py

commit 4f3c19ffb5
Author: James Vecellio <alexgrant990@gmail.com>
Date:   Wed Nov 19 20:28:59 2025 -0800

    save the trees we got icons

commit ae5c44355d
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Wed Nov 19 13:38:04 2025 -0800

    the heck

commit 066438ad10
Merge: 9532675814 e74460f3a8
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Wed Nov 19 12:18:17 2025 -0800

    Merge remote-tracking branch 'origin/rl-tree-dialog' into rl-tree-dialog

commit 9532675814
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Wed Nov 19 12:17:52 2025 -0800

    less trees for the planet

commit e74460f3a8
Merge: c347db376a 423a7d2ed0
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Wed Nov 19 09:37:44 2025 -0800

    Merge branch 'rl-sp-toggles' into rl-tree-dialog

commit c347db376a
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Wed Nov 19 09:36:33 2025 -0800

    tree dialog

commit c9bd67b261
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Wed Nov 19 09:34:08 2025 -0800

    merge origin raylib toggles

commit d7b8ce86ed
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 17 20:21:33 2025 -0800

    compare vs what used to be done before InputDialog

commit 2d3d104658
Merge: ded02895f4 f1025f6ee9
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Mon Nov 17 19:24:20 2025 -0800

    Merge branch 'master' into input-dialog

commit ded02895f4
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Mon Nov 17 19:22:01 2025 -0800

    dialog txt

commit 9778a925b0
Merge: cb03d08397 08e85808c5
Author: Jason Wen <haibin.wen3@gmail.com>
Date:   Sun Nov 16 03:16:58 2025 -0500

    input dialog

commit 423a7d2ed0
Author: nayan <nayan8teen@gmail.com>
Date:   Sun Nov 16 11:15:28 2025 -0500

    fix ui preview

commit e4e10d4b87
Author: nayan <nayan8teen@gmail.com>
Date:   Sun Nov 16 11:15:22 2025 -0500

    fix callback

commit 362e9ce04b
Author: nayan <nayan8teen@gmail.com>
Date:   Sun Nov 16 09:53:28 2025 -0500

    sp raylib preview

commit 3946e643f6
Author: nayan <nayan8teen@gmail.com>
Date:   Sat Nov 15 20:24:20 2025 -0500

    optimizations

commit 0c37a38596
Author: nayan <nayan8teen@gmail.com>
Date:   Sat Nov 15 09:42:12 2025 -0500

    Lint

commit 9c5acf61c0
Author: nayan <nayan8teen@gmail.com>
Date:   Sat Nov 15 09:29:07 2025 -0500

    SP Toggles

commit 121b304fe0
Author: nayan <nayan8teen@gmail.com>
Date:   Sat Nov 15 09:28:58 2025 -0500

    init styles

commit 47d848293b
Author: nayan <nayan8teen@gmail.com>
Date:   Sat Nov 15 09:28:43 2025 -0500

    param to control stock vs sp ui

* Squashed commit of the following:

commit 70ad001add
Merge: 142663c490 844f4cbc74
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Mon Nov 24 11:54:58 2025 -0800

    Merge branch 'master' into rl-progress-bar

commit 142663c490
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Sat Nov 22 20:12:44 2025 -0800

    smoother updating

commit 4476e418dd
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Sat Nov 22 09:55:17 2025 -0800

    easier to see

commit ad66c22e88
Merge: 0c46ef5948 457b6634fd
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Sat Nov 22 07:45:05 2025 -0800

    Merge branch 'master' into rl-progress-bar

commit 0c46ef5948
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Sat Nov 22 07:42:59 2025 -0800

    freaking test dir

commit 11c19aad24
Merge: 7785238d54 8184cd8a6a
Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Date:   Fri Nov 21 12:01:36 2025 -0800

    Merge branch 'master' into rl-progress-bar

commit 7785238d54
Author: discountchubbs <alexgrant990@gmail.com>
Date:   Wed Nov 19 07:47:10 2025 -0800

    raylib: progress bar

* OSM panel

* fetching

* only show if fav_param is used in the call

* flattened and custom search query

* conditional for mypy

* sunny's new x,y makes this even easier!

* download all

* add back the rough estimate

* not sure i like the 'Download'

* simplify the path

* actual size as of today

* format

* more simple

* only show on download or delete

* loathing loathing, unadulterated loathing, i loathe it all

* loathing loathing, unadulterated loathing, i loathe it all

* # Conflicts:
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/tree_dialog.py

* search

* st

* Update osm.py

* one second updates:

 its heavy process, which isnt really noticeable during downloads ayways. the once a second ensures responsiveness on the ui, while also maintaining 20fps on device for country/state downloads.

* efficient? i hope

* big boi texts

* big boi texts

* use our own classes

* need to clear all params when delete all

* more

* collateral lol

* do not behave as selected if canceled during US->States dialog

* more

* instead of timestamp, let's just show formatted time

* disable button when downloading dbs

* should be the buttons being disabled

* well gotta re-enable them too

* empty country

* might be bigger now

* fixes for mapd manager

* should stay as a json

* sanitize it a bit

* revert

* only nuke if the cancel button is called

* always try to update the labels

* Revert "always try to update the labels"

This reverts commit ba0988fc06.

* re-enable button after download is complete

* disable all while downloading (till we could cancel and re-download)

* fix progress bar not filling up as intended for smaller total counts

* revert

* use new

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-12 02:40:33 -05:00
Jason Wen
928672999b ui: consolidate NoElideButtonAction (#1569) 2025-12-12 02:31:06 -05:00
Shane Smiskol
9947206ccd comma four: fix wrapping steer right (#36848)
rm extra space
2025-12-11 21:52:24 -08:00
James Vecellio-Grant
b2e7dffa59 modeld_v2: support planplus outputs (#1532)
* conditional concatenation

* v13

* modifucatuins
2025-12-11 23:24:45 -05:00
Bruce Wayne
0871a35c10 Revert "Dark Souls Model (#36764)"
This reverts commit 83dad85cdd.
2025-12-11 19:43:53 -08:00
Suyog Shinde
2d91aa5abc locationd: fix velocity calibration using wrong pose field (#36844) 2025-12-11 19:24:32 -08:00
Matt Purnell
13693e3a0a loggerd: Fix test that fails on non-TICI devices (#36846)
Only check for TICI files on TICI
2025-12-11 18:19:59 -08:00
Shane Smiskol
edede31c32 athenad: get ES256 key (#36845)
* fix

* why not format

* fix typing

* cast
2025-12-11 18:00:43 -08:00
Adeeb Shihadeh
c61ed10015 USB GPU benchmarking (#36840)
* test boot time

* lil nicer

* cleanup

* revert that

---------

Co-authored-by: Comma Device <device@comma.ai>
2025-12-11 13:04:59 -08:00
Adeeb Shihadeh
1391434f54 setup: fix uv install fail (#36839)
* pipefail

* curl retry
2025-12-11 11:22:08 -08:00
YassineYousfi
d8125f50d2 dm: speedup stat filters convergence (#36756)
* dm: speedup stat filters convergence

* lint
2025-12-11 09:38:53 -08:00
Dean Lee
a49273d9d4 remove unused #include "common/params.h" from hardware.h (#36827)
remove include
2025-12-11 09:22:30 -08:00
rj-lynch
ff5b75d164 Refactor CarSpecificEvents Class extracting BRAND_EXTRA_GEARS (#36805)
* Brand Extra Gears Dict added. Gear data removed from CarSpecificEvents Update method, data now held in global variable.

* Added elif for Ford and Nissan events creation. BRAND_EXTRA_GEARS now extracted from CarSpecificEvents

* Amended Chrysler and Toyota create_common_events calls.

* format

* can do this!

* consis

* whoops

* type

---------

Co-authored-by: RJ <ryan@DESKTOP-4S0L5O5.localdomain>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-12-10 16:35:35 -08:00
Shane Smiskol
53b7adedc2 Fix UI timing test (#36823)
* why did no one tell me about this?!

* not necessary
2025-12-10 00:29:03 -08:00
Shane Smiskol
f78bacf96b mici ui replay: temp remove swipes (#36818)
hmm it IS nondeterm
2025-12-09 15:38:43 -08:00
Shane Smiskol
dfd56a46d2 mici cameraview: log timings (#36816)
missing from mici
2025-12-09 15:37:13 -08:00
clintonsteiner
6bbc3f4d1c pyproject: remove pytools pinning (#36812)
* pyproject: remove pytools pinning

* issue requiring pin is fixed
* https://github.com/inducer/pyopencl/issues/827

* uv lock

---------

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-12-09 11:04:03 -08:00
Harald Schäfer
34fed9f908 URLFILE: Need to catch max retry (#36815)
Need to catch max retry
2025-12-09 10:19:10 -08:00
Adeeb Shihadeh
c85db43705 camerad: misc labeling/cleanup (#36809)
* what's 2c

* include

* no idea what this means

* register comments
2025-12-08 20:14:19 -08:00
Shane Smiskol
8d9e203130 raylib ui diff: swipe around (#36807)
* swipe support

* swipe around

* same
2025-12-08 19:25:09 -08:00
Adeeb Shihadeh
d5f6946502 camerad: probe os first 2025-12-08 18:48:58 -08:00
「 crwusiz 」
48a42a9c53 UI: Color Constants Uppercase (#36796) 2025-12-08 18:43:31 -08:00
Adeeb Shihadeh
fb807cc007 ui: video diff tool (#36737)
* video diff

* format

* duplicate

* try

* WINDOWED

* ?

* correct res

* Revert "correct res"

This reverts commit f90991192fce93a31d1b581a4f0ff93a7a972337.

* save to report/

* add duplicate

* work?

* fix

* more

* more

* and this

* ffmpeg

* branch

* uncmt

* test preview

* Revert "uncmt"

This reverts commit b02404dbbe515fd861717f831c7bb0243442ddbc.

* create openpilot_master_ui_mici_raylib

* ahh

* push to master

* copy and always run

* test

* does cmt break it?

* who did this

* fix?

* fix that

* hmm

* hmm

* ah this was moving it, and then the job below didn't run on master

* google ai overview lied to me

* use markdown to start

* need to add to one branch

* ????

* oof

* no

* this work?

* test

* try this

* clean up master branch name

* more cleanup

more cleanup

* don't fail for no diff!

don't fail for no diff!

* back

* add to cmt

* test it

* should work

* fix that

* back

* clean up

* clean up

* save to report

* pull_request_target

* sort

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-12-08 18:39:47 -08:00
Matt Purnell
7119412d35 updated: fix skipped test case (#36786)
Fix three failing tests
2025-12-08 16:41:45 -08:00
Chechulin Serhii
fadf7ff1e5 ui: feature Ukrainian translation (#36646)
* Add Ukrainian lang

* update_translations.py

* Add Ukrainian strings

* Small patch to display translated update states

* Revert "Small patch to display translated update states"

This reverts commit b0545f4e109f451a21e4e5884259dbb881d7a58e.

* Revert "update_translations.py"

This reverts commit 79eea20c33f1b1d542b62a782ab1b67bc9277026.

* fix so these meaningless edits
2025-12-08 16:40:29 -08:00
Matt Purnell
cce2e4d357 tools: Handle smaller terminal sizes in replay (#36766)
* Only show help if there's room for it

* show less

* wording
2025-12-08 16:38:35 -08:00
Dean Lee
4e74e0f755 cabana: fix UI hang when switching streams (#36735)
fix UI hang when switching streams
2025-12-08 16:36:55 -08:00
Dean Lee
a6645a1be1 cabana: add automatic session save/restore (#36736)
adds auto session save/store
2025-12-08 16:36:35 -08:00
「 crwusiz 」
239d690a43 Multilang: update kor translation (#36795) 2025-12-08 16:32:56 -08:00
Shane Smiskol
d4d6134d3b UnifiedLabel: fix clipping descenders (#36793)
* fix

* can also do this

* but then y is off. this is from font_scale I think

* fix

* cmt
2025-12-05 18:18:58 -08:00
Robbe Derks
0965650f61 Bump panda (#36783)
* panda bump

* try this one

* this breaks it?

* still broken, right?

* fixed?

* second try
2025-12-05 23:12:39 +01:00
Shane Smiskol
224e2c271b Revert "ui: fix dialog memory leak" (#36787)
Revert "ui: fix dialog memory leak (#36767)"

This reverts commit 45b7d60263.
2025-12-04 16:51:18 -08:00
Harald Schäfer
e72e5d4ebe beeps in key (#36765)
beeps in keyt
2025-12-04 13:11:27 -08:00
Shane Smiskol
f962a36fd8 Fix ui crashing replay/selfdrived (#36760)
* fix

* clean up

* type hint
2025-12-04 02:39:41 -08:00
Dean Lee
2947af42fc ui: fix TraningGuide leak (#36763)
* fix TraningGuide leak

* other thing

* this is truly the simplest way

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-12-04 02:23:37 -08:00
Shane Smiskol
cd9b08492e ui: small TrainingGuide clean up 2025-12-04 02:00:45 -08:00
Shane Smiskol
45b7d60263 ui: fix dialog memory leak (#36767)
* weakref alternative

* and here

* clean up

* fix

* rm
2025-12-04 01:56:38 -08:00
Dean Lee
93f2076c7e ui: fix crash caused by double shader unload in CameraView (#36778)
fix double free isuue
2025-12-04 01:54:11 -08:00
Shane Smiskol
4edbc7d0cf DriverCameraDialog: proper clean up (#36775)
* fixes leak

* wait can't do this, we need close after all

* wait can't do this, we need close after all

* clean up memory
2025-12-03 22:19:30 -08:00
Shane Smiskol
cc7dd066d2 ui: call modal hide_event (#36772)
* start, not fully working since hide is called before last render

* clean up
2025-12-03 21:55:05 -08:00
Shane Smiskol
9e55577cc7 Clean up DM dialog CameraView bound method (#36770)
* clean up

* why not?

* clean up
2025-12-03 20:41:58 -08:00
Shane Smiskol
7ea6cfcbdf remove unecessary function 2025-12-03 20:00:19 -08:00
Harald Schäfer
83dad85cdd Dark Souls Model (#36764)
a4cf2707-3d69-49ea-af8b-f91cd3285249/400
2025-12-03 12:55:33 -08:00
Maxime Desroches
5fd0906164 allow restarting processes after crash (#36755)
more
2025-12-02 17:10:24 -08:00
Maxime Desroches
e7d349bf36 Revert "ui: restart if crash (#36746)" (#36754)
This reverts commit fa18bb9261.
2025-12-02 16:45:09 -08:00
Bruce Wayne
5393308d03 Logreader: print errors 2025-12-02 15:54:21 -08:00
YassineYousfi
dc02a2d385 dm: adjust cold start pose offsets (#36739)
* dm: adjust cold start offsets and thresholds

* change just offsets for now
2025-12-02 15:17:59 -08:00
Chechulin Serhii
63563c3561 ui: fix - translate display text of updater_state (#36649)
* Add updater_state translation

* Move STATE_TO_DISPLAY_TEXT on top
2025-12-02 13:13:13 -08:00
Dean Lee
ae6250e685 ui/CameraView: use consistent 2-space indentation (#36748)
use consistent 2-space indentation
2025-12-02 13:09:24 -08:00
Shane Smiskol
ae402d3ac7 Revert "ui: speed up mici/AugmentedRoadView by optimizing _calc_frame_matrix caching" (#36749)
Revert "ui: speed up `mici/AugmentedRoadView` by optimizing _calc_frame_matri…"

This reverts commit 1052435391.
2025-12-02 13:02:01 -08:00
Harald Schäfer
cfb0a1c18c URLFile multirange (#36740)
* url file multirange

* cleanup urlfile

* time

* fixup

* raise

* Diskfile
2025-12-01 23:11:03 -08:00
Maxime Desroches
fa18bb9261 ui: restart if crash (#36746)
* simpler

* mypy your are going to be replaced very soon
2025-12-01 22:55:14 -08:00
Trey Moen
cabfa7b735 Revert "esim: remove bootstrap and delete (#36732)" (#36747)
This reverts commit 6d04251517.
2025-12-01 22:45:11 -08:00
Maxime Desroches
65e551c671 Handle invalid frame fd when creating EGL image (#36743)
catch
2025-12-01 21:32:07 -08:00
Maxime Desroches
62b7abcd91 Fix raylib ui spamming API calls (#36745)
fix
2025-12-01 21:13:43 -08:00
Maxime Desroches
dc654b439a Revert "Fix raylib ui spamming API calls (#36700)" (#36744)
This reverts commit 26261387f8.
2025-12-01 20:48:04 -08:00
Dean Lee
693c83f74c replay: fix dangling pointers in logging calls (#36738)
fix dangling pointers in logging calls
2025-12-01 13:32:21 -08:00
Trey Moen
8ffe3f287e fix: openpilot build on ubuntu aarch64 (#36675)
breaks on linux
2025-11-30 16:09:50 -08:00
Dean Lee
749e236bc0 ui: fix EGL_BAD_MATCH error when running profile_onroad.py on device (#36608)
fix failed to create EGL image:12297 error on device
2025-11-30 16:06:33 -08:00
Adeeb Shihadeh
151d256dd6 add param for agnos power monitor 2025-11-30 15:29:40 -08:00
Adeeb Shihadeh
436e3dec3e manager: write power monitor flag atomically (#36734) 2025-11-30 15:14:31 -08:00
Adeeb Shihadeh
7521fd11e2 common: rename atomic_write_in_dir -> atomic_write (#36733)
rename
2025-11-30 15:08:32 -08:00
MVL
ff755ed4bf Honda - Rename AcuraWatch Plus to AcuraWatch (#36726)
* Rename AcuraWatch Plus to AcuraWatch

* Rename AcuraWatch Plus to AcuraWatch
2025-11-30 15:04:17 -08:00
Trey Moen
6d04251517 esim: remove bootstrap and delete (#36732)
init
2025-11-30 14:43:29 -08:00
Adeeb Shihadeh
970afa9683 bump to 0.10.3 2025-11-30 14:19:37 -08:00
David
cd7e362333 ui: Add RECORD=1 for direct frame recording (#36729)
* ui: add real-time video recording functionality with ffmpeg support

* fix: record at consistent frame rate

* add spaces

* fix type

* refactor: RECORD_FRAMES variable and related logic

* fix: remove unnecessary texture check

* support missing output extension

* add wait for close with timeout

* fix: ensure RECORD_OUTPUT has the correct file extension

* flush on close and terminate if times out closing

* ffmpeg hide banner

* reduce ffmpeg spam

* refactor: streamline ffmpeg arguments for video encoding

* refactor: move size arg to variable and add yub420p conversion for native support

* use render_width and render_height for size

* fix: ensure even dimensions for video encoding when recording

* rm itertools

* simple

* cleanup

* docs

---------

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-11-30 13:34:37 -08:00
Shane Smiskol
85a162dd43 more scons nodes 2025-11-30 02:48:05 -08:00
Dean Lee
8de8946374 ui: skip _draw_set_speed when alpha is 0 (#36709)
* Skip _draw_set_speed when alpha is 0 to reduce unnecessary draw calls

* Update selfdrive/ui/mici/onroad/hud_renderer.py

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-11-29 03:15:41 -08:00
Dean Lee
f1c2b1df7f ui: fix CameraView crash in mici due to stale frame (#36710)
fix CameraView crash caused by stale frame
2025-11-29 03:14:23 -08:00
Shane Smiskol
1b20567c98 Mici keyboard: alpha filter for key bg (#36720)
* filter

* tune

* fix
2025-11-29 02:30:32 -08:00
Dean Lee
6c39f6bb53 ui: Fix scroll logic for non-scrollable content (bounds_size > content_size) to prevent jitter (#36693)
* Fix scroll logic for non-scrollable content to prevent jitter

* one thing

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-11-29 02:21:15 -08:00
Shane Smiskol
22003fd10a rl.BLANK 2025-11-29 02:15:38 -08:00
Shane Smiskol
088fc1cab1 Unified label: add scrolling (#36717)
* almost

* works!

* clean up

* fix

* trash

* Revert "trash"

This reverts commit 951d63382810d444fe08103f406a8c490cfcbe25.

* fix some bugs and use

* clean up

* clean up

* fix clipping

* clean up

* fix
2025-11-29 02:15:10 -08:00
Shane Smiskol
cb718618d1 fix multi option dialog text centering 2025-11-29 02:12:09 -08:00
Shane Smiskol
d6de3572ca UnifiedLabel: split render (#36719)
* split

* rect
2025-11-29 01:26:17 -08:00
Shane Smiskol
d8c316faef Fix wifi settings NavWidget 2025-11-29 00:56:43 -08:00
Shane Smiskol
65f18c363b Mici advanced network settings (#36716)
* add back

* forgot

* clean up
2025-11-28 23:41:55 -08:00
Shane Smiskol
c32e2898ac mici: split wifi and network settings (#36715)
* split

* clean up

* better
2025-11-28 23:29:13 -08:00
Harald Schäfer
d3532d7d6f URLFile: catch more (#36712)
* catch

* linter has a point
2025-11-28 17:25:54 -08:00
Jason Young
f07a40deb4 regen CARS.md (#36711) 2025-11-28 13:08:07 -05:00
Dean Lee
1052435391 ui: speed up mici/AugmentedRoadView by optimizing _calc_frame_matrix caching (#36669)
speed up AugmentedRoadView by optimizing _calc_frame_matrix caching
2025-11-27 05:58:53 -08:00
Shane Smiskol
ce596424cf Fix steering arc artifacts (#36707)
* fix arc artifacts

* works but how

* also this

* Revert "also this"

This reverts commit e8d5ed9af15568dcb178dd6da7a14d2c6191010e.

* clean up

* nl

* clean up

* more

* print

* print
2025-11-27 04:16:38 -08:00
Shane Smiskol
3959200a5b Scroll panel 2: use float for offset (#36705)
* use float internally

* use it everywhere -- this fixes all the problemS?!

* round it everywhere

* this looks so much better than before

* rm
2025-11-27 03:25:30 -08:00
Dean Lee
f8d0f22344 ui: ensure auto-scroll stops correctly near target (#36686)
ensure auto-scroll stops correctly near target
2025-11-27 03:22:15 -08:00
Calvin Park
0a0fadb16a Skip onboarding on PC (#36688)
* Skip onboarding on PC

* do this instead

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-11-27 02:44:13 -08:00
Dean Lee
d0489062b5 ui: remove unused members and variables (#36677)
remove unused members and variables
2025-11-27 02:28:51 -08:00
Dean Lee
630e14fd7f ui: avoid unnecessary text cache invalidation in UnifiedLabel (#36676)
avoid unnecessary text cache invalidation in UnifiedLabel
2025-11-27 02:28:13 -08:00
Najib Muhammad
394f580f16 fix the CI Weekly Report workflow so it does not fail on forks (#36664) 2025-11-27 02:26:29 -08:00
Dean Lee
4ef82c4119 ui: optimize matrix operations in scroller rendering (#36668)
optimize matrix operations
2025-11-27 02:20:49 -08:00
Dean Lee
4bd6fb0995 ui: fix unconditional rl.end_scissor_mode() call in MiciLabel (#36660)
fix incorrect end_scissor_mode usage
2025-11-27 02:17:16 -08:00
Logesh R
ae534ddeab docs: Fix "Turn the speed blue" tutorial for Raylib UI (#36591)
* docs: Fix "Turn the speed blue" tutorial for Raylib UI

* just

* obv

* not replay

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-11-27 02:07:56 -08:00
David
ae6ada4162 lint: Add PLE rule to ruff (#36595)
* update linting rules to include new PLE (pylint error) rule

* fix lint error
2025-11-27 01:52:25 -08:00
Dean Lee
b8d55987c2 ui: extract and optimize mouse event processing (#36564)
* extract and optimize mouse event processing

* rm slot

* merge mici mastere

* add mouse

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2025-11-27 01:50:36 -08:00
Shane Smiskol
26261387f8 Fix raylib ui spamming API calls (#36700)
* intern

* start

* move

* common caching

* use constant for slep

* works

* add gating back

* clean up

* more

* match cache logic

* hate this circular

* not needed since sync

* no need for lock?

* even qt had something like _load_initial_state for tests, keep

* clean up

* clean up

* clean up

* loading json as string works, else it will fail to parse json, catch that and log, and next api call will overwrite

* move over firehose

* clean up

* fix test

* no

* flip

* more

* match qt

* consistent

* clean up

* cmt

* fix test!
2025-11-27 01:37:16 -08:00
Shane Smiskol
946fd3f387 NavWidget: draw black above top of widget when dismissing (#36702)
draw rec
2025-11-27 01:00:33 -08:00
Shane Smiskol
50a797b0be De-duplicate firehose layout (#36703)
* Reapply "De-duplicate firehose layout (#36701)"

This reverts commit 302e448b93.

* fix

* was here
2025-11-27 01:00:03 -08:00
Shane Smiskol
302e448b93 Revert "De-duplicate firehose layout (#36701)"
This reverts commit dd51bf2021.
2025-11-27 00:52:50 -08:00
Shane Smiskol
dd51bf2021 De-duplicate firehose layout (#36701)
* consistent name

* dedup

* FMT

* not sure why two
2025-11-27 00:46:33 -08:00
YassineYousfi
49178539f3 dm: DriverProb (#36687)
* wip

* ci

* fix
2025-11-25 18:52:35 -08:00
felsager
f01391a7d9 latcontrol_torque: delay independent jerk and lower kp and lower friction threshold (#36619) 2025-11-25 10:23:02 -08:00
Bruce Wayne
c67afb45ae dead test 2025-11-24 14:20:20 -08:00
Adeeb Shihadeh
d0c3972cc7 update release branches (#36671)
* update release branches

* Update README.md
2025-11-21 18:55:01 -08:00
Adeeb Shihadeh
be8c5491b1 even shorter 2025-11-21 14:25:21 -08:00
Adeeb Shihadeh
ebc11fdbc8 make "update available" alert clickable (#36670)
* click to update

* that's it

* lil more
2025-11-21 13:44:22 -08:00
Adeeb Shihadeh
a981f78e2f use release branch from system.version 2025-11-21 11:23:54 -08:00
127 changed files with 3756 additions and 866 deletions

View File

@@ -38,7 +38,7 @@ jobs:
report:
needs: [ci_matrix_run]
runs-on: ubuntu-latest
if: always()
if: always() && github.repository == 'commaai/openpilot'
steps:
- name: Get job results
uses: actions/github-script@v7

View File

@@ -0,0 +1,151 @@
name: "mici raylib ui preview"
on:
push:
branches:
- master
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master'
paths:
- 'selfdrive/assets/**'
- 'selfdrive/ui/**'
- 'system/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create mici 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 }}-mici-raylib-ui"
MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib"
# All report files are pushed here
REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports"
jobs:
preview:
if: github.repository == 'commaai/openpilot'
name: preview
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: write
actions: read
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Waiting for ui generation to end
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ env.SHA }}
check-name: ${{ env.UI_JOB_NAME }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
allowed-conclusions: success
wait-interval: 20
- name: Getting workflow run ID
id: get_run_id
run: |
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
- name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4
id: download-artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ steps.get_run_id.outputs.run_id }}
search_artifacts: true
name: mici-raylib-report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/master_ui_raylib
ref: ${{ env.MASTER_BRANCH_NAME }}
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git checkout --orphan=new_master_ui_mici_raylib
git rm -rf *
git branch -D ${{ env.MASTER_BRANCH_NAME }}
git branch -m ${{ env.MASTER_BRANCH_NAME }}
git config user.name "GitHub Actions Bot"
git config user.email "<>"
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "mici raylib video for commit ${{ env.SHA }}"
git push origin ${{ env.MASTER_BRANCH_NAME }} --force
- name: Setup FFmpeg
uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae
- name: Finding diff
if: github.event_name == 'pull_request_target'
id: find_diff
run: |
# Find the video file from PR
pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4"
mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video"
master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4"
mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video"
# Run report
export PYTHONPATH=${{ github.workspace }}
baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}"
diff_exit_code=0
python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$?
# Copy diff report files
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/
REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html"
if [ $diff_exit_code -eq 0 ]; then
DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)"
else
DIFF="❌ <strong>Videos differ!</strong> [View Diff Report]($REPORT_URL)"
fi
echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT"
- name: Saving proposed ui
if: github.event_name == 'pull_request_target'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
# Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference
git config user.name "GitHub Actions Bot"
git config user.email "<>"
git checkout --orphan=${{ env.BRANCH_NAME }}
git rm -rf *
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "mici raylib video for PR #${{ github.event.number }}"
git push origin ${{ env.BRANCH_NAME }} --force
# Append diff report to report files branch
git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }}
git checkout ${{ env.REPORT_FILES_BRANCH_NAME }}
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html
git add diff_pr_${{ github.event.number }}.html
git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit"
git push origin ${{ env.REPORT_FILES_BRANCH_NAME }}
- name: Comment Video on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
<!-- _(run_id_video_mici_raylib **${{ github.run_id }}**)_ -->
## mici raylib UI Preview
${{ steps.find_diff.outputs.DIFF }}
comment_tag: run_id_video_mici_raylib
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -200,37 +200,28 @@ jobs:
sudo rm -rf ${OUTPUT_DIR}
mkdir -p ${OUTPUT_DIR}
rsync -am${RUNNER_DEBUG:+v} \
--include='**/panda/board/' \
--include='**/panda/board/obj' \
--include='**/panda/board/obj/panda.bin.signed' \
--include='**/panda/board/obj/panda_h7.bin.signed' \
--include='**/panda/board/obj/bootstub.panda.bin' \
--include='**/panda/board/obj/bootstub.panda_h7.bin' \
--exclude='.sconsign.dblite' \
--exclude='*.a' \
--exclude='*.o' \
--exclude='*.os' \
--exclude='*.pyc' \
--exclude='moc_*' \
--exclude='*.cc' \
--exclude='__pycache__' \
--exclude='Jenkinsfile' \
--exclude='supercombo.onnx' \
--exclude='**/panda/board/*' \
--exclude='**/panda/board/obj/**' \
--exclude='**/panda/certs/' \
--exclude='**/panda/crypto/' \
--exclude='**/release/' \
--exclude='**/.github/' \
--exclude='**/selfdrive/ui/replay/' \
--exclude='**/__pycache__/' \
--exclude='**/selfdrive/ui/*.h' \
--exclude='**/selfdrive/ui/**/*.h' \
--exclude='**/selfdrive/ui/qt/offroad/sunnypilot/' \
--exclude='${{env.SCONS_CACHE_DIR}}' \
--exclude='**/.git/' \
--exclude='**/SConstruct' \
--exclude='**/SConscript' \
--exclude='**/.venv/' \
--exclude='selfdrive/modeld/models/driving_vision.onnx' \
--exclude='selfdrive/modeld/models/driving_policy.onnx' \
--exclude='sunnypilot/modeld*/models/supercombo.onnx' \
--exclude='third_party/*x86*' \
--exclude='third_party/*Darwin*' \
--delete-excluded \
--chown=comma:comma \
${BUILD_DIR}/ ${OUTPUT_DIR}/

View File

@@ -297,3 +297,29 @@ jobs:
with:
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
create_mici_raylib_ui_report:
name: Create mici 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"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Create mici raylib UI Report
run: >
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py"
- name: Upload Raylib UI Report
uses: actions/upload-artifact@v4
with:
name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/diff/report

View File

@@ -1,3 +1,6 @@
Version 0.10.3 (2025-12-10)
========================
Version 0.10.2 (2025-11-19)
========================
* comma four support

View File

@@ -192,6 +192,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
aTarget @5 :Float32;
events @6 :List(OnroadEventSP.Event);
e2eAlerts @7 :E2eAlerts;
accelPersonality @8 :AccelerationPersonality;
struct DynamicExperimentalControl {
state @0 :DynamicExperimentalControlState;
@@ -203,7 +204,11 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
blended @1;
}
}
enum AccelerationPersonality {
sport @0;
normal @1;
eco @2;
}
struct SmartCruiseControl {
vision @0 :Vision;
map @1 :Map;

View File

@@ -22,5 +22,5 @@ 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():
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
return CommaConnectApi(None).get_key_pair()

View File

@@ -6,9 +6,9 @@ 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"}
# name: jwt signature algorithm
KEYS = {"id_rsa": "RS256",
"id_ecdsa": "ES256"}
class BaseApi:
@@ -62,7 +62,7 @@ class BaseApi:
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
@staticmethod
def get_key_pair():
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
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:

View File

@@ -1 +1 @@
#define DEFAULT_MODEL "The Cool People (Default)"
#define DEFAULT_MODEL "Dark Souls 2 (Default)"

View File

@@ -71,6 +71,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
{"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
{"LastAgnosPowerMonitorShutdown", {CLEAR_ON_MANAGER_START, STRING}},
{"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}},
{"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}},
{"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
@@ -132,6 +133,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},

View File

@@ -1,7 +1,7 @@
import os
from uuid import uuid4
from openpilot.common.utils import atomic_write_in_dir
from openpilot.common.utils import atomic_write
class TestFileHelpers:
@@ -15,5 +15,5 @@ class TestFileHelpers:
assert f.read() == "test"
os.remove(path)
def test_atomic_write_in_dir(self):
self.run_atomic_write_func(atomic_write_in_dir)
def test_atomic_write(self):
self.run_atomic_write_func(atomic_write)

View File

@@ -32,8 +32,8 @@ class CallbackReader:
@contextlib.contextmanager
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
overwrite: bool = False):
def atomic_write(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)

View File

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

View File

@@ -20,7 +20,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2017">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2018-21">Buy Here</a></sub></details>|||
@@ -384,7 +384,7 @@ If your car has the following packages or features, then it's a good candidate f
| Make | Required Package/Features |
| ---- | ------------------------- |
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. |
| Ford | Any car with Lane Centering will likely work. |
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |

View File

@@ -31,7 +31,7 @@ We'll run the `replay` tool with the demo route to get data streaming for testin
tools/replay/replay --demo
# in terminal 2
selfdrive/ui/ui
./selfdrive/ui/ui.py
```
The openpilot UI should launch and show a replay of the demo route.
@@ -43,39 +43,36 @@ If you have your own comma device, you can replace `--demo` with one of your own
Now lets update the speed display color in the UI.
Search for the function responsible for rendering UI text:
Search for the function responsible for rendering the current speed:
```bash
git grep "drawText" selfdrive/ui/qt/onroad/hud.cc
git grep "_draw_current_speed" selfdrive/ui/onroad/hud_renderer.py
```
Youll find the relevant code inside `selfdrive/ui/qt/onroad/hud.cc`, in this function:
You'll find the relevant code inside `selfdrive/ui/onroad/hud_renderer.py`, in this function:
```cpp
void HudRenderer::drawText(QPainter &p, int x, int y, const QString &text, int alpha) {
QRect real_rect = p.fontMetrics().boundingRect(text);
real_rect.moveCenter({x, y - real_rect.height() / 2});
p.setPen(QColor(0xff, 0xff, 0xff, alpha)); // <- this sets the speed text color
p.drawText(real_rect.x(), real_rect.bottom(), text);
}
```python
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
"""Draw the current vehicle speed and unit."""
speed_text = str(round(self.speed))
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
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) # <- this sets the speed text color
```
Change the `QColor(...)` line to make it **blue** instead of white. A nice soft blue is `#8080FF`, which translates to:
Change `COLORS.white` to make it **blue** instead of white. A nice soft blue is `#8080FF`, which you can change inline:
```diff
- p.setPen(QColor(0xff, 0xff, 0xff, alpha));
+ p.setPen(QColor(0x80, 0x80, 0xFF, alpha));
- rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
+ rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, rl.Color(0x80, 0x80, 0xFF, 255))
```
This change will tint all speed-related UI text to blue with the same transparency (`alpha`).
---
## 4. Rebuild the UI
## 4. Re-run the UI
After making changes, rebuild Openpilot so your new UI is compiled:
After making changes, re-run the UI to see your new UI:
```bash
scons -j$(nproc) && selfdrive/ui/ui
./selfdrive/ui/ui.py
```
![](https://blog.comma.ai/img/blue_speed_ui.png)

2
panda

Submodule panda updated: dee9061b2a...5f3c09c910

View File

@@ -85,6 +85,7 @@ docs = [
]
testing = [
"coverage",
"hypothesis ==6.47.*",
"mypy",
"pytest",
@@ -115,7 +116,7 @@ dev = [
"pyautogui",
"pygame",
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
"pytools>=2025.1.6; platform_machine != 'aarch64'",
"pywinctl",
"pyprof2calltree",
"tabulate",
@@ -125,7 +126,7 @@ dev = [
tools = [
"metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')",
"dearpygui>=2.1.0",
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
]
[project.urls]
@@ -226,7 +227,7 @@ lint.select = [
"TRY203", "TRY400", "TRY401", # try/excepts
"RUF008", "RUF100",
"TID251",
"PLR1704",
"PLE", "PLR1704",
]
lint.ignore = [
"E741",

26
scripts/usbgpu/benchmark.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
cd $DIR/../../tinygrad_repo
GREEN='\033[0;32m'
NC='\033[0m'
#export DEBUG=2
export PYTHONPATH=.
export AM_RESET=1
export AMD=1
export AMD_IFACE=USB
export AMD_LLVM=1
python3 -m unittest -q --buffer test.test_tiny.TestTiny.test_plus \
> /tmp/test_tiny.log 2>&1 || (cat /tmp/test_tiny.log; exit 1)
printf "${GREEN}Booted in ${SECONDS}s${NC}\n"
printf "${GREEN}=============${NC}\n"
printf "\n\n"
printf "${GREEN}Transfer speeds:${NC}\n"
printf "${GREEN}================${NC}\n"
python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,19 @@
import numpy as np
from scipy.io import wavfile
sr = 48000
max_int16 = 2**15 - 1
def harmonic_beep(freq, duration_seconds):
n_total = int(sr * duration_seconds)
signal = np.sin(2 * np.pi * freq * np.arange(n_total) / sr)
x = np.arange(n_total)
exp_scale = np.exp(-x/5.5e3)
return max_int16 * signal * exp_scale
engage_beep = harmonic_beep(1661.219, 0.5)
wavfile.write("engage.wav", sr, engage_beep.astype(np.int16))
disengage_beep = harmonic_beep(1318.51, 0.5)
wavfile.write("disengage.wav", sr, disengage_beep.astype(np.int16))

View File

@@ -42,7 +42,7 @@ If your car has the following packages or features, then it's a good candidate f
| Make | Required Package/Features |
| ---- | ------------------------- |
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. |
| Ford | Any car with Lane Centering will likely work. |
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |

View File

@@ -26,6 +26,18 @@ class MockCarState:
return CS, CS_SP
BRAND_EXTRA_GEARS = {
'ford': [GearShifter.low, GearShifter.manumatic],
'nissan': [GearShifter.brake],
'chrysler': [GearShifter.low],
'honda': [GearShifter.sport],
'toyota': [GearShifter.sport],
'gm': [GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic],
'volkswagen': [GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
'hyundai': [GearShifter.sport, GearShifter.manumatic]
}
class CarSpecificEvents:
def __init__(self, CP: structs.CarParams):
self.CP = CP
@@ -36,17 +48,13 @@ class CarSpecificEvents:
self.silent_steer_warning = True
def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl):
extra_gears = BRAND_EXTRA_GEARS.get(self.CP.brand, None)
if self.CP.brand in ('body', 'mock'):
events = Events()
elif self.CP.brand == 'ford':
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low, GearShifter.manumatic])
elif self.CP.brand == 'nissan':
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.brake])
elif self.CP.brand == 'chrysler':
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low])
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
# Low speed steer alert hysteresis logic
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
@@ -57,7 +65,7 @@ class CarSpecificEvents:
events.add(EventName.belowSteerSpeed)
elif self.CP.brand == 'honda':
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False)
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=False)
if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed:
events.add(EventName.belowEngageSpeed)
@@ -79,7 +87,7 @@ class CarSpecificEvents:
elif self.CP.brand == 'toyota':
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport])
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
if self.CP.openpilotLongitudinalControl:
if CS.cruiseState.standstill and not CS.brakePressed:
@@ -94,9 +102,7 @@ class CarSpecificEvents:
events.add(EventName.manualRestart)
elif self.CP.brand == 'gm':
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport, GearShifter.low,
GearShifter.eco, GearShifter.manumatic],
pcm_enable=self.CP.pcmCruise)
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
# Enabling at a standstill with brake is allowed
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
@@ -107,8 +113,7 @@ class CarSpecificEvents:
events.add(EventName.resumeRequired)
elif self.CP.brand == 'volkswagen':
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
pcm_enable=self.CP.pcmCruise)
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
if self.CP.openpilotLongitudinalControl:
if CS.vEgo < self.CP.minEnableSpeed + 0.5:
@@ -121,15 +126,14 @@ class CarSpecificEvents:
# events.add(EventName.steerTimeLimit)
elif self.CP.brand == 'hyundai':
events = self.create_common_events(CS, CS_prev, extra_gears=(GearShifter.sport, GearShifter.manumatic),
pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
else:
events = self.create_common_events(CS, CS_prev)
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
return events
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears=None, pcm_enable=True,
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True,
allow_button_cancel=True):
events = Events()

View File

@@ -22,15 +22,17 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La
# Additionally, there is friction in the steering wheel that needs
# to be overcome to move it at all, this is compensated for too.
KP = 1.0
KI = 0.3
KD = 0.0
KP = 0.8
KI = 0.15
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
JERK_LOOKAHEAD_SECONDS = 0.19
JERK_GAIN = 0.3
LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0
VERSION = 0
VERSION = 1
class LatControlTorque(LatControl):
def __init__(self, CP, CP_SP, CI, dt):
@@ -38,13 +40,13 @@ class LatControlTorque(LatControl):
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([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt)
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, 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.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt)
self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
@@ -76,17 +78,15 @@ class LatControlTorque(LatControl):
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
lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))
raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt)
desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk)
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
setpoint = expected_lateral_accel
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
@@ -94,15 +94,10 @@ class LatControlTorque(LatControl):
ff = gravity_adjusted_future_lateral_accel
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
ff -= self.torque_params.latAccelOffset
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, 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,
-measurement_rate,
feedforward=ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator)
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
# Lateral acceleration torque controller extension updates

View File

@@ -10,6 +10,8 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.modeld.constants import index_function
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
if __name__ == '__main__': # generating code
from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
else:
@@ -228,6 +230,7 @@ class LongitudinalMpc:
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.reset()
self.source = SOURCES[2]
self.accel_controller = AccelPersonalityController()
def reset(self):
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
@@ -332,6 +335,13 @@ class LongitudinalMpc:
v_ego = self.x0[1]
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
if self.accel_controller.is_enabled():
min_accel = self.accel_controller.get_min_accel(v_ego)
else:
min_accel = CRUISE_MIN_ACCEL
a_cruise_min = min_accel
lead_xv_0 = self.process_lead(radarstate.leadOne)
lead_xv_1 = self.process_lead(radarstate.leadTwo)
@@ -350,7 +360,7 @@ class LongitudinalMpc:
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
# when the leads are no factor.
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
# TODO does this make sense when max_a is negative?
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),

View File

@@ -124,7 +124,11 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
if mode == 'acc':
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
if self.accel_controller.is_enabled():
max_accel = self.accel_controller.get_max_accel(v_ego)
accel_clip = [ACCEL_MIN, max_accel]
else:
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
else:

View File

@@ -172,7 +172,7 @@ class PoseCalibrator:
ned_from_calib_euler = self._ned_from_calib(pose.orientation)
angular_velocity_calib = self._transform_calib_from_device(pose.angular_velocity)
acceleration_calib = self._transform_calib_from_device(pose.acceleration)
velocity_calib = self._transform_calib_from_device(pose.angular_velocity)
velocity_calib = self._transform_calib_from_device(pose.velocity)
return Pose(ned_from_calib_euler, velocity_calib, acceleration_calib, angular_velocity_calib)

View File

@@ -1,102 +0,0 @@
import numpy as np
import random
import cereal.messaging as messaging
from msgq.visionipc import VisionIpcServer, VisionStreamType
from opendbc.car.car_helpers import get_demo_car_params
from openpilot.common.params import Params
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.realtime import DT_MDL
from openpilot.system.manager.process_config import managed_processes
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam
IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8)
IMG_BYTES = IMG.flatten().tobytes()
class TestModeld:
def setup_method(self):
self.vipc_server = VisionIpcServer("camerad")
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height)
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height)
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height)
self.vipc_server.start_listener()
Params().put("CarParams", get_demo_car_params().to_bytes())
self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry'])
self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration'])
managed_processes['modeld'].start()
self.pm.wait_for_readers_to_update("roadCameraState", 10)
def teardown_method(self):
managed_processes['modeld'].stop()
del self.vipc_server
def _send_frames(self, frame_id, cams=None):
if cams is None:
cams = ('roadCameraState', 'wideRoadCameraState')
cs = None
for cam in cams:
msg = messaging.new_message(cam)
cs = getattr(msg, cam)
cs.frameId = frame_id
cs.timestampSof = int((frame_id * DT_MDL) * 1e9)
cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9))
cam_meta = meta_from_camera_state(cam)
self.pm.send(msg.which(), msg)
self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId,
cs.timestampSof, cs.timestampEof)
return cs
def _wait(self):
self.sm.update(5000)
if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId:
self.sm.update(1000)
def test_modeld(self):
for n in range(1, 500):
cs = self._send_frames(n)
self._wait()
mdl = self.sm['modelV2']
assert mdl.frameId == n
assert mdl.frameIdExtra == n
assert mdl.timestampEof == cs.timestampEof
assert mdl.frameAge == 0
assert mdl.frameDropPerc == 0
odo = self.sm['cameraOdometry']
assert odo.frameId == n
assert odo.timestampEof == cs.timestampEof
def test_dropped_frames(self):
"""
modeld should only run on consecutive road frames
"""
frame_id = -1
road_frames = list()
for n in range(1, 50):
if (random.random() < 0.1) and n > 3:
cams = random.choice([(), ('wideRoadCameraState', )])
self._send_frames(n, cams)
else:
self._send_frames(n)
road_frames.append(n)
self._wait()
if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1:
frame_id = road_frames[-1]
mdl = self.sm['modelV2']
odo = self.sm['cameraOdometry']
assert mdl.frameId == frame_id
assert mdl.frameIdExtra == frame_id
assert odo.frameId == frame_id
if n != frame_id:
assert not self.sm.updated['modelV2']
assert not self.sm.updated['cameraOdometry']

View File

@@ -40,8 +40,8 @@ def dmonitoringd_thread():
# save rhd virtual toggle every 5 mins
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
DM.wheelpos_learner.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
DM.wheel_on_right == (DM.wheelpos_learner.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
def main():

View File

@@ -40,6 +40,9 @@ class DRIVER_MONITOR_SETTINGS:
self._PHONE_THRESH2 = 15.0
self._PHONE_MAX_OFFSET = 0.06
self._PHONE_MIN_OFFSET = 0.025
self._PHONE_DATA_AVG = 0.05
self._PHONE_DATA_VAR = 3*0.005
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
@@ -47,9 +50,11 @@ class DRIVER_MONITOR_SETTINGS:
self._POSE_YAW_THRESHOLD = 0.4020
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
self._PITCH_NATURAL_THRESHOLD = 0.449
self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
self._PITCH_NATURAL_VAR = 3*0.01
self._YAW_NATURAL_VAR = 3*0.05
self._PITCH_MAX_OFFSET = 0.124
self._PITCH_MIN_OFFSET = -0.0881
self._YAW_MAX_OFFSET = 0.289
@@ -70,6 +75,9 @@ class DRIVER_MONITOR_SETTINGS:
self._WHEELPOS_CALIB_MIN_SPEED = 11
self._WHEELPOS_THRESHOLD = 0.5
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side
self._WHEELPOS_DATA_AVG = 0.03
self._WHEELPOS_DATA_VAR = 3*5.5e-5
self._WHEELPOS_MAX_COUNT = -1
self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change
self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change
@@ -78,30 +86,33 @@ class DRIVER_MONITOR_SETTINGS:
self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts
class DistractedType:
NOT_DISTRACTED = 0
DISTRACTED_POSE = 1 << 0
DISTRACTED_BLINK = 1 << 1
DISTRACTED_PHONE = 1 << 2
class DriverPose:
def __init__(self, max_trackable):
def __init__(self, settings):
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
self.yaw = 0.
self.pitch = 0.
self.roll = 0.
self.yaw_std = 0.
self.pitch_std = 0.
self.roll_std = 0.
self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable)
self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable)
self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.calibrated = False
self.low_std = True
self.cfactor_pitch = 1.
self.cfactor_yaw = 1.
class DriverPhone:
def __init__(self, max_trackable):
class DriverProb:
def __init__(self, raw_priors, max_trackable):
self.prob = 0.
self.prob_offseter = RunningStatFilter(max_trackable=max_trackable)
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
self.prob_calibrated = False
class DriverBlink:
@@ -140,9 +151,11 @@ class DriverMonitoring:
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
# init driver status
self.wheelpos_learner = RunningStatFilter()
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
self.phone = DriverPhone(self.settings._POSE_OFFSET_MAX_COUNT)
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
self.pose = DriverPose(settings=self.settings)
self.blink = DriverBlink()
self.always_on = always_on
@@ -234,8 +247,11 @@ class DriverMonitoring:
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
yaw_error = abs(yaw_error)
if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \
yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw:
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
distracted_types.append(DistractedType.DISTRACTED_POSE)
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
@@ -256,9 +272,12 @@ class DriverMonitoring:
# calibrates only when there's movement and either face detected
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
self.wheelpos_learner.push_and_update(rhd_pred)
if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT or demo_mode:
self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
self.wheelpos.prob_offseter.push_and_update(rhd_pred)
self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT
if self.wheelpos.prob_calibrated or demo_mode:
self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
else:
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
# make sure no switching when engaged

View File

@@ -1 +1 @@
b508f43fb0481bce0859c9b6ab4f45ee690b8dab
e0ad86508edb61b3eaa1b84662c515d2c3368295

View File

@@ -206,8 +206,9 @@ class TestOnroad:
result += "-------------- UI Draw Timing ------------------\n"
result += "------------------------------------------------\n"
# skip first few frames -- connecting to vipc
ts = self.ts['uiDebug']['drawTimeMillis'][15:]
# other processes preempt ui while starting up
offset = int(20 * LOG_OFFSET)
ts = self.ts['uiDebug']['drawTimeMillis'][offset:]
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"

View File

@@ -1,19 +1,11 @@
import pyray as rl
import time
import threading
from openpilot.common.api import api_get
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, 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
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase
TITLE = tr_noop("Firehose Mode")
DESCRIPTION = tr_noop(
@@ -32,50 +24,17 @@ INSTRUCTIONS = tr_noop(
)
class FirehoseLayout(Widget):
PARAM_KEY = "ApiCache_FirehoseStats"
GREEN = rl.Color(46, 204, 113, 255)
RED = rl.Color(231, 76, 60, 255)
GRAY = rl.Color(68, 68, 68, 255)
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
UPDATE_INTERVAL = 30 # seconds
class FirehoseLayout(FirehoseLayoutBase):
def __init__(self):
super().__init__()
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:
return 0
try:
return int(stats.get("firehose", 0))
except Exception:
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
return 0
def __del__(self):
self.running = False
if self.update_thread and self.update_thread.is_alive():
self.update_thread.join(timeout=1.0)
self._scroll_panel = GuiScrollPanel()
def _render(self, rect: rl.Rectangle):
# Calculate content dimensions
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
# Handle scrolling and render with clipping
scroll_offset = self.scroll_panel.update(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._content_height = self._render_content(rect, scroll_offset)
rl.end_scissor_mode()
@@ -107,9 +66,9 @@ class FirehoseLayout(Widget):
y += 20 + 20
# Contribution count (if available)
if self.segment_count > 0:
if self._segment_count > 0:
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20 + 20
@@ -121,7 +80,7 @@ class FirehoseLayout(Widget):
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
# bottom margin + remove effect of scroll offset
return int(round(y - self.scroll_panel.offset + 40))
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)
@@ -129,32 +88,3 @@ class FirehoseLayout(Widget):
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 tr("ACTIVE"), self.GREEN
else:
return tr("INACTIVE: connect to an unmetered network"), self.RED
def _fetch_firehose_stats(self):
try:
dongle_id = self.params.get("DongleId")
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
identity_token = get_token(dongle_id)
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
if response.status_code == 200:
data = response.json()
self.segment_count = data.get("firehose", 0)
self.params.put(self.PARAM_KEY, data)
except Exception as e:
cloudlog.error(f"Failed to fetch firehose stats: {e}")
def _update_loop(self):
while self.running:
if not ui_state.started:
self._fetch_firehose_stats()
time.sleep(self.UPDATE_INTERVAL)

View File

@@ -17,6 +17,13 @@ if gui_app.sunnypilot_ui():
# TODO: remove this. updater fails to respond on startup if time is not correct
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
# Mapping updater internal states to translated display strings
STATE_TO_DISPLAY_TEXT = {
"checking...": tr("checking..."),
"downloading...": tr("downloading..."),
"finalizing update...": tr("finalizing update..."),
}
def time_ago(date: datetime.datetime | None) -> str:
if not date:
@@ -103,7 +110,9 @@ class SoftwareLayout(Widget):
# Updater responded
self._waiting_for_updater = False
self._download_btn.action_item.set_enabled(False)
self._download_btn.action_item.set_value(updater_state)
# Use the mapping, with a fallback to the original state string
display_text = STATE_TO_DISPLAY_TEXT.get(updater_state, updater_state)
self._download_btn.action_item.set_value(display_text)
else:
if failed_count > 0:
self._download_btn.action_item.set_value(tr("failed to check for update"))

View File

@@ -67,8 +67,10 @@ class PrimeState:
cloudlog.info(f"Prime type updated to {prime_type}")
def _worker_thread(self) -> None:
from openpilot.selfdrive.ui.ui_state import ui_state, device
while self._running:
self._fetch_prime_status()
if not ui_state.started and device._awake:
self._fetch_prime_status()
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
if not self._running:

View File

@@ -3,18 +3,16 @@ import time
from cereal import log
import pyray as rl
from collections.abc import Callable
from openpilot.system.ui.widgets.label import gui_label, MiciLabel
from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.text import wrap_text
from openpilot.system.version import training_version
from openpilot.system.version import training_version, RELEASE_BRANCHES
HEAD_BUTTON_FONT_SIZE = 40
HOME_PADDING = 8
RELEASE_BRANCH = "release3"
NetworkType = log.DeviceState.NetworkType
NETWORK_TYPES = {
@@ -115,7 +113,7 @@ class MiciHomeLayout(Widget):
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._branch_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN, elide_right=False, scroll=True)
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
def show_event(self):
@@ -187,27 +185,22 @@ class MiciHomeLayout(Widget):
if self._version_text is not None:
# release branch
if self._version_text[0] == RELEASE_BRANCH:
version_pos = rl.Vector2(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16)
self._large_version_label.set_text(self._version_text[0])
self._large_version_label.set_position(version_pos.x, version_pos.y)
self._large_version_label.render()
release_branch = self._version_text[1] in RELEASE_BRANCHES
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
self._version_label.set_text(self._version_text[0])
self._version_label.set_position(version_pos.x, version_pos.y)
self._version_label.render()
else:
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
self._version_label.set_text(self._version_text[0])
self._version_label.set_position(version_pos.x, version_pos.y)
self._version_label.render()
self._date_label.set_text(" " + self._version_text[3])
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
self._date_label.render()
self._date_label.set_text(" " + self._version_text[3])
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
self._date_label.render()
self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
self._branch_label.set_text(" " + self._version_text[1])
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
self._branch_label.render()
self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1]))
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
self._branch_label.render()
if not release_branch:
# 2nd line
self._version_commit_label.set_text(self._version_text[2])
self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7)

View File

@@ -5,6 +5,7 @@ from dataclasses import dataclass
from enum import IntEnum
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import Scroller
@@ -220,6 +221,7 @@ class MiciOffroadAlerts(Widget):
update_alert_data = AlertData(key="UpdateAvailable", text="", severity=-1)
self.sorted_alerts.append(update_alert_data)
update_alert_item = AlertItem(update_alert_data)
update_alert_item.set_click_callback(lambda: HARDWARE.reboot())
self.alert_items.append(update_alert_item)
self._scroller.add_widget(update_alert_item)
@@ -244,18 +246,18 @@ class MiciOffroadAlerts(Widget):
if update_alert_data:
if update_available:
# Default text
update_alert_data.text = "update available. go to comma.ai/blog to read the release notes."
version_string = ""
# Get new version description and parse version and date
new_desc = self.params.get("UpdaterNewDescription") or ""
if new_desc:
# Parse description (format: "version / branch / commit / date")
# format: "version / branch / commit / date"
parts = new_desc.split(" / ")
if len(parts) > 3:
version, date = parts[0], parts[3]
update_alert_data.text = f"update available\n sunnypilot {version}, {date}. go to comma.ai/blog to read the release notes."
version_string = f"\nsunnypilot {version}, {date}\n"
update_alert_data.text = f"Update available {version_string}. Click to update. Read the release notes at blog.comma.ai."
update_alert_data.visible = True
active_count += 1
else:

View File

@@ -1,6 +1,7 @@
from enum import IntEnum
from collections.abc import Callable
import weakref
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets import Widget
@@ -92,11 +93,10 @@ class TrainingGuideDMTutorial(Widget):
super().__init__()
self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
self._original_continue_callback = continue_callback
# Wrap the continue callback to restore settings
def wrapped_continue_callback():
self._restore_settings()
device.set_offroad_brightness(None)
device.reset_interactive_timeout()
continue_callback()
self._dialog = DriverCameraSetupDialog(wrapped_continue_callback)
@@ -114,10 +114,6 @@ class TrainingGuideDMTutorial(Widget):
device.set_offroad_brightness(100)
device.reset_interactive_timeout(300) # 5 minutes
def _restore_settings(self):
device.set_offroad_brightness(None)
device.reset_interactive_timeout()
def _update_state(self):
super()._update_state()
if device.awake:
@@ -150,7 +146,7 @@ class TrainingGuideRecordFront(SetupTermsPage):
super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
self._dm_label = UnifiedLabel("Do you want to upload driver camera data to improve driver monitoring?", 42,
self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
FontWeight.ROMAN)
def show_event(self):
@@ -214,11 +210,17 @@ class TrainingGuide(Widget):
self._completed_callback = completed_callback
self._step = 0
self_ref = weakref.ref(self)
def on_continue():
if obj := self_ref():
obj._advance_step()
self._steps = [
TrainingGuideAttentionNotice(continue_callback=self._advance_step),
TrainingGuidePreDMTutorial(continue_callback=self._advance_step),
TrainingGuideDMTutorial(continue_callback=self._advance_step),
TrainingGuideRecordFront(continue_callback=self._advance_step),
TrainingGuideAttentionNotice(continue_callback=on_continue),
TrainingGuidePreDMTutorial(continue_callback=on_continue),
TrainingGuideDMTutorial(continue_callback=on_continue),
TrainingGuideRecordFront(continue_callback=on_continue),
]
def _advance_step(self):

View File

@@ -39,7 +39,7 @@ class MiciFccModal(NavWidget):
content_height += self._fcc_logo.height + 20
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
scroll_offset = self._scroll_panel.update(rect, scroll_content_rect.height)
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)

View File

@@ -6,14 +6,13 @@ from openpilot.common.api import api_get
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.api_helpers import get_token
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
from openpilot.system.ui.widgets import NavWidget
from openpilot.system.ui.widgets import Widget, NavWidget
TITLE = tr_noop("Firehose Mode")
DESCRIPTION = tr_noop(
@@ -34,9 +33,7 @@ FAQ_ITEMS = [
]
class FirehoseLayoutMici(NavWidget):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
class FirehoseLayoutBase(Widget):
PARAM_KEY = "ApiCache_FirehoseStats"
GREEN = rl.Color(46, 204, 113, 255)
RED = rl.Color(231, 76, 60, 255)
@@ -44,12 +41,10 @@ class FirehoseLayoutMici(NavWidget):
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
UPDATE_INTERVAL = 30 # seconds
def __init__(self, back_callback):
def __init__(self):
super().__init__()
self.set_back_callback(back_callback)
self.params = Params()
self.segment_count = self._get_segment_count()
self._params = Params()
self._segment_count = self._get_segment_count()
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._content_height = 0
@@ -71,7 +66,7 @@ class FirehoseLayoutMici(NavWidget):
self._scroll_panel.set_offset(0)
def _get_segment_count(self) -> int:
stats = self.params.get(self.PARAM_KEY)
stats = self._params.get(self.PARAM_KEY)
if not stats:
return 0
try:
@@ -83,7 +78,7 @@ class FirehoseLayoutMici(NavWidget):
def _render(self, rect: rl.Rectangle):
# compute total content height for scrolling
content_height = self._measure_content_height(rect)
scroll_offset = self._scroll_panel.update(rect, content_height)
scroll_offset = round(self._scroll_panel.update(rect, content_height))
# start drawing with offset
x = int(rect.x + 40)
@@ -111,9 +106,9 @@ class FirehoseLayoutMici(NavWidget):
y += 20
# Contribution count (if available)
if self.segment_count > 0:
if self._segment_count > 0:
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
"{} 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), 42, rl.WHITE)
y += 20
@@ -165,9 +160,9 @@ class FirehoseLayoutMici(NavWidget):
y += int(len(status_lines) * 48 * FONT_SCALE) + 20
# Contribution count
if self.segment_count > 0:
if self._segment_count > 0:
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 42, w)
y += int(len(contrib_lines) * 42 * FONT_SCALE) + 20
@@ -204,20 +199,28 @@ class FirehoseLayoutMici(NavWidget):
def _fetch_firehose_stats(self):
try:
dongle_id = self.params.get("DongleId")
dongle_id = self._params.get("DongleId")
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
identity_token = get_token(dongle_id)
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
if response.status_code == 200:
data = response.json()
self.segment_count = data.get("firehose", 0)
self.params.put(self.PARAM_KEY, data)
self._segment_count = data.get("firehose", 0)
self._params.put(self.PARAM_KEY, data)
except Exception as e:
cloudlog.error(f"Failed to fetch firehose stats: {e}")
def _update_loop(self):
while self._running:
if not ui_state.started:
if not ui_state.started and device._awake:
self._fetch_firehose_stats()
time.sleep(self.UPDATE_INTERVAL)
class FirehoseLayout(FirehoseLayoutBase, NavWidget):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
def __init__(self, back_callback):
super().__init__()
self.set_back_callback(back_callback)

View File

@@ -0,0 +1,182 @@
import pyray as rl
from enum import IntEnum
from collections.abc import Callable
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import NavWidget
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
class NetworkPanelType(IntEnum):
NONE = 0
WIFI = 1
class NetworkLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
super().__init__()
self._current_panel = NetworkPanelType.WIFI
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
_tethering_icon = "icons_mici/settings/network/tethering.png"
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.set_modal_overlay(dlg)
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** IP Address ********
self._ip_address_btn = BigButton("IP Address", "Not connected")
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
wifi_button = BigButton("wi-fi")
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
# ******** Advanced settings ********
# ******** Roaming toggle ********
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
# ******** APN settings ********
self._apn_btn = BigButton("apn settings", "edit")
self._apn_btn.set_click_callback(self._edit_apn)
# ******** Cellular metered toggle ********
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller = Scroller([
wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
# /* Advanced settings
self._roaming_btn,
self._apn_btn,
self._cellular_metered_btn,
# */
self._ip_address_btn,
], snap_items=False)
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
metered = ui_state.params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
# Set up back navigation
self.set_back_callback(back_callback)
def _update_state(self):
super()._update_state()
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
self._wifi_manager.set_ipv4_forward(show_cell_settings)
self._roaming_btn.set_visible(show_cell_settings)
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
def show_event(self):
super().show_event()
self._current_panel = NetworkPanelType.NONE
self._wifi_ui.show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._wifi_ui.hide_event()
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(apn: str):
apn = apn.strip()
if apn == "":
ui_state.params.remove("GsmApn")
else:
ui_state.params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.set_modal_overlay(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update IP address
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
def _switch_to_panel(self, panel_type: NetworkPanelType):
self._current_panel = panel_type
def _render(self, rect: rl.Rectangle):
self._wifi_manager.process_callbacks()
if self._current_panel == NetworkPanelType.WIFI:
self._wifi_ui.render(rect)
else:
self._scroller.render(rect)

View File

@@ -1,28 +1,20 @@
import math
import numpy as np
import pyray as rl
from enum import IntEnum
from collections.abc import Callable
from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.widgets import Widget, NavWidget
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, MeteredType
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType
def normalize_ssid(ssid: str) -> str:
return ssid.replace("", "'") # for iPhone hotspots
class NetworkPanelType(IntEnum):
NONE = 0
WIFI = 1
class LoadingAnimation(Widget):
def _render(self, _):
cx = int(self._rect.x + 70)
@@ -91,11 +83,13 @@ class WifiIcon(Widget):
class WifiItem(BigDialogOptionButton):
LEFT_MARGIN = 20
HEIGHT = 54
SELECTED_HEIGHT = 74
def __init__(self, network: Network):
super().__init__(network.ssid)
self.set_rect(rl.Rectangle(0, 0, gui_app.width, 64))
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96)
@@ -103,6 +97,10 @@ class WifiItem(BigDialogOptionButton):
self._wifi_icon = WifiIcon()
self._wifi_icon.set_current_network(network)
def set_selected(self, selected: bool):
super().set_selected(selected)
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
def set_current_network(self, network: Network):
self._network = network
self._wifi_icon.set_current_network(network)
@@ -117,7 +115,7 @@ class WifiItem(BigDialogOptionButton):
self._wifi_icon.render(rl.Rectangle(
self._rect.x + self.LEFT_MARGIN,
self._rect.y,
self._rect.height,
self.SELECTED_HEIGHT,
self._rect.height
))
@@ -126,7 +124,7 @@ class WifiItem(BigDialogOptionButton):
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._label.set_font_weight(FontWeight.DISPLAY)
else:
self._label.set_font_size(70)
self._label.set_font_size(54)
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
@@ -215,7 +213,7 @@ class NetworkInfoPage(NavWidget):
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True)
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
@@ -225,6 +223,10 @@ class NetworkInfoPage(NavWidget):
self._network: Network | None = None
self._connecting: Callable[[], str | None] | None = None
def show_event(self):
super().show_event()
self._title.reset_scroll()
def update_networks(self, networks: dict[str, Network]):
# update current network from latest scan results
for ssid, network in networks.items():
@@ -392,7 +394,7 @@ class WifiUIMici(BigMultiOptionDialog):
self._network_info_page.set_current_network(_network)
self._should_open_network_info_page = True
network_button.set_click_callback(lambda _net=network,_button=network_button: _button._selected and show_network_info_page(_net))
network_button.set_click_callback(lambda _net=network, _button=network_button: _button._selected and show_network_info_page(_net))
self.add_button(network_button)
@@ -443,116 +445,3 @@ class WifiUIMici(BigMultiOptionDialog):
if not self._networks:
self._loading_animation.render(self._rect)
class NetworkLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
super().__init__()
self._current_panel = NetworkPanelType.WIFI
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
_tethering_icon = "icons_mici/settings/network/tethering.png"
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.set_modal_overlay(dlg)
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** IP Address ********
self._ip_address_btn = BigButton("IP Address", "Not connected")
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
wifi_button = BigButton("wi-fi")
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
# Main scroller ----------------------------------
self._scroller = Scroller([
wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
self._ip_address_btn,
], snap_items=False)
# Set up back navigation
self.set_back_callback(back_callback)
def show_event(self):
super().show_event()
self._current_panel = NetworkPanelType.NONE
self._wifi_ui.show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._wifi_ui.hide_event()
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update IP address
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
def _switch_to_panel(self, panel_type: NetworkPanelType):
self._current_panel = panel_type
def _render(self, rect: rl.Rectangle):
self._wifi_manager.process_callbacks()
if self._current_panel == NetworkPanelType.WIFI:
self._wifi_ui.render(rect)
else:
self._scroller.render(rect)

View File

@@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget, NavWidget
@@ -67,7 +67,7 @@ class SettingsLayout(NavWidget):
PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))),
}
self._font_medium = gui_app.font(FontWeight.MEDIUM)

View File

@@ -89,10 +89,6 @@ ALERT_CRITICAL_REBOOT = Alert(
class AlertRenderer(Widget):
def __init__(self):
super().__init__()
self.font_regular: rl.Font = gui_app.font(FontWeight.MEDIUM)
self.font_roman: rl.Font = gui_app.font(FontWeight.ROMAN)
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self.font_display: rl.Font = gui_app.font(FontWeight.DISPLAY)
self._alert_text1_label = UnifiedLabel(text="", font_size=ALERT_FONT_BIG, font_weight=FontWeight.DISPLAY, line_height=0.86,
letter_spacing=-0.02)
@@ -204,11 +200,11 @@ class AlertRenderer(Widget):
text_x = self._rect.x + ALERT_MARGIN
text_width = self._rect.width - ALERT_MARGIN
if icon_side == 'left':
text_x = self._rect.x + self._txt_turn_signal_right.width + 20 * 2
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2
text_x = self._rect.x + self._txt_turn_signal_right.width
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width
elif icon_side == 'right':
text_x = self._rect.x + ALERT_MARGIN
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width
text_rect = rl.Rectangle(
text_x,

View File

@@ -1,6 +1,7 @@
import time
import numpy as np
import pyray as rl
from cereal import car, log
from cereal import messaging, car, log
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
@@ -160,6 +161,9 @@ class AugmentedRoadView(CameraView):
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
# debug
self._pm = messaging.PubMaster(['uiDebug'])
def is_swiping_left(self) -> bool:
"""Check if currently swiping left (for scroller to disable)."""
return self._bookmark_icon.is_swiping_left()
@@ -179,6 +183,7 @@ class AugmentedRoadView(CameraView):
super()._handle_mouse_release(mouse_pos)
def _render(self, _):
start_draw = time.monotonic()
self._switch_stream_if_needed(ui_state.sm)
# Update calibration before rendering
@@ -244,6 +249,11 @@ class AugmentedRoadView(CameraView):
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
self._offroad_label.render(self._content_rect)
# publish uiDebug
msg = messaging.new_message('uiDebug')
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
self._pm.send('uiDebug', msg)
def _switch_stream_if_needed(self, sm):
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
v_ego = sm['carState'].vEgo

View File

@@ -107,7 +107,6 @@ 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)
@@ -197,7 +196,10 @@ class CameraView(Widget):
# Clean up shader
if self.shader and self.shader.id:
rl.unload_shader(self.shader)
self.shader.id = 0
self.frame = None
self.available_streams.clear()
self.client = None
def __del__(self):
@@ -234,6 +236,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

@@ -15,20 +15,27 @@ EventName = log.OnroadEvent.EventName
EVENT_TO_INT = EventName.schema.enumerants
class DriverCameraView(CameraView):
def _calc_frame_matrix(self, rect: rl.Rectangle):
base = super()._calc_frame_matrix(rect)
driver_view_ratio = 1.5
base[0, 0] *= driver_view_ratio
base[1, 1] *= driver_view_ratio
return base
class DriverCameraDialog(NavWidget):
def __init__(self, no_escape=False):
super().__init__()
self._camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
self._original_calc_frame_matrix = self._camera_view._calc_frame_matrix
self._camera_view._calc_frame_matrix = self._calc_driver_frame_matrix
self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
self.driver_state_renderer = DriverStateRenderer(lines=True)
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200))
self.driver_state_renderer.load_icons()
self._pm = messaging.PubMaster(['selfdriveState'])
self._pm: messaging.PubMaster | None = None
if not no_escape:
# TODO: this can grow unbounded, should be given some thought
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
self.set_back_callback(self._dismiss)
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_enabled(not no_escape)
# Load eye icons
@@ -40,26 +47,24 @@ class DriverCameraDialog(NavWidget):
self._load_eye_textures()
def stop_dmonitoringmodeld(self):
ui_state.params.put_bool("IsDriverViewEnabled", False)
gui_app.set_modal_overlay(None)
def show_event(self):
super().show_event()
ui_state.params.put_bool("IsDriverViewEnabled", True)
self._publish_alert_sound(None)
device.reset_interactive_timeout(300)
ui_state.params.remove("DriverTooDistracted")
self._pm = messaging.PubMaster(['selfdriveState'])
def hide_event(self):
super().hide_event()
ui_state.params.put_bool("IsDriverViewEnabled", False)
device.reset_interactive_timeout()
def _handle_mouse_release(self, _):
ui_state.params.remove("DriverTooDistracted")
def _dismiss(self):
self.stop_dmonitoringmodeld()
def __del__(self):
self.close()
def close(self):
if self._camera_view:
@@ -103,6 +108,9 @@ class DriverCameraDialog(NavWidget):
def _publish_alert_sound(self, dm_state):
"""Publish selfdriveState with only alertSound field set"""
if self._pm is None:
return
msg = messaging.new_message('selfdriveState')
if dm_state is not None and len(dm_state.events):
event_name = EVENT_TO_INT[dm_state.events[0].name]
@@ -221,13 +229,6 @@ class DriverCameraDialog(NavWidget):
glasses_prob = driver_data.sunglassesProb
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
def _calc_driver_frame_matrix(self, rect: rl.Rectangle):
base = self._original_calc_frame_matrix(rect)
driver_view_ratio = 1.5
base[0, 0] *= driver_view_ratio
base[1, 1] *= driver_view_ratio
return base
if __name__ == "__main__":
gui_app.init_window("Driver Camera View (mici)")

View File

@@ -3,6 +3,7 @@ from collections.abc import Callable
import numpy as np
import math
from cereal import log
from openpilot.system.hardware import PC
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
@@ -217,7 +218,10 @@ class DriverStateRenderer(Widget):
rotation = math.degrees(math.atan2(pitch, yaw))
angle_diff = rotation - self._rotation_filter.x
angle_diff = ((angle_diff + 180) % 360) - 180
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
if PC and self._confirm_mode:
self._rotation_filter.x += 2
else:
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
if not self.should_draw:
self._fade_filter.update(0.0)

View File

@@ -30,20 +30,8 @@ class FontSizes:
@dataclass(frozen=True)
class Colors:
white: rl.Color = rl.WHITE
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
engaged: rl.Color = rl.Color(128, 216, 166, 255)
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
grey: rl.Color = rl.Color(166, 166, 166, 255)
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
header_gradient_end: rl.Color = rl.BLANK
WHITE = rl.WHITE
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
FONT_SIZES = FontSizes()
@@ -236,16 +224,18 @@ class HudRenderer(Widget):
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
"""Draw the MAX speed indicator box."""
x = rect.x
y = rect.y
alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
self._can_draw_top_icons and self._engaged)
if alpha < 1e-2:
return
x = rect.x
y = rect.y
# draw drop shadow
circle_radius = 162 // 2
rl.draw_circle_gradient(int(x + circle_radius), int(y + circle_radius), circle_radius,
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.Color(0, 0, 0, 0))
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK)
set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
max_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
@@ -279,9 +269,9 @@ class HudRenderer(Widget):
speed_text = str(round(self.speed))
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
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)
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
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)
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)

View File

@@ -130,6 +130,9 @@ def arc_bar_pts(cx: float, cy: float,
pts = np.vstack((outer, cap_end, inner, cap_start, outer[:1])).astype(np.float32)
# Rotate to start from middle of cap for proper triangulation
pts = np.roll(pts, cap_segs, axis=0)
if DEBUG:
n = len(pts)
idx = int(time.monotonic() * 12) % max(1, n) # speed: 12 pts/sec

View File

@@ -282,7 +282,8 @@ class BigDialogOptionButton(Widget):
self._selected = False
self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
scroll=True)
def set_selected(self, selected: bool):
self._selected = selected

View File

@@ -68,7 +68,6 @@ 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)
@@ -337,12 +336,12 @@ class CameraView(Widget):
self._initialize_textures()
def _initialize_textures(self):
self._clear_textures()
if not TICI:
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
self._clear_textures()
if not TICI:
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
def _clear_textures(self):
if self.texture_y and self.texture_y.id:

View File

@@ -35,20 +35,20 @@ class FontSizes:
@dataclass(frozen=True)
class Colors:
white: rl.Color = rl.WHITE
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
engaged: rl.Color = rl.Color(128, 216, 166, 255)
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
grey: rl.Color = rl.Color(166, 166, 166, 255)
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
header_gradient_end: rl.Color = rl.BLANK
WHITE = rl.WHITE
DISENGAGED = rl.Color(145, 155, 149, 255)
OVERRIDE = rl.Color(145, 155, 149, 255) # Added
ENGAGED = rl.Color(128, 216, 166, 255)
DISENGAGED_BG = rl.Color(0, 0, 0, 153)
OVERRIDE_BG = rl.Color(145, 155, 149, 204)
ENGAGED_BG = rl.Color(128, 216, 166, 204)
GREY = rl.Color(166, 166, 166, 255)
DARK_GREY = rl.Color(114, 114, 114, 255)
BLACK_TRANSLUCENT = rl.Color(0, 0, 0, 166)
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
BORDER_TRANSLUCENT = rl.Color(255, 255, 255, 75)
HEADER_GRADIENT_START = rl.Color(0, 0, 0, 114)
HEADER_GRADIENT_END = rl.BLANK
UI_CONFIG = UIConfig()
@@ -108,8 +108,8 @@ class HudRenderer(Widget):
int(rect.y),
int(rect.width),
UI_CONFIG.header_height,
COLORS.header_gradient_start,
COLORS.header_gradient_end,
COLORS.HEADER_GRADIENT_START,
COLORS.HEADER_GRADIENT_END,
)
if self.is_cruise_available:
@@ -131,19 +131,19 @@ 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.35, 10, COLORS.black_translucent)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 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
max_color = COLORS.GREY
set_speed_color = COLORS.DARK_GREY
if self.is_cruise_set:
set_speed_color = COLORS.white
set_speed_color = COLORS.WHITE
if ui_state.status == UIStatus.ENGAGED:
max_color = COLORS.engaged
max_color = COLORS.ENGAGED
elif ui_state.status == UIStatus.DISENGAGED:
max_color = COLORS.disengaged
max_color = COLORS.DISENGAGED
elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.override
max_color = COLORS.OVERRIDE
max_text = tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
@@ -172,9 +172,9 @@ class HudRenderer(Widget):
speed_text = str(round(self.speed))
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
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)
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
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)
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)

View File

@@ -0,0 +1,106 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import datetime
import os
from pathlib import Path
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import button_item
from openpilot.system.ui.sunnypilot.widgets.html_render import HtmlModalSP
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
PREBUILT_PATH = os.path.join(Paths.comma_home(), "prebuilt") if PC else "/data/openpilot/prebuilt"
class DeveloperLayoutSP(DeveloperLayout):
def __init__(self):
super().__init__()
self.error_log_path = os.path.join(Paths.crash_log_root(), "error.log")
self._is_release_branch: bool = self._is_release or ui_state.params.get_bool("IsReleaseSpBranch")
self._is_development_branch: bool = ui_state.params.get_bool("IsTestedBranch") or ui_state.params.get_bool("IsDevelopmentBranch")
self._initialize_items()
for item in self.items:
self._scroller.add_widget(item)
def _initialize_items(self):
self.show_advanced_controls = toggle_item_sp(tr("Show Advanced Controls"),
tr("Toggle visibility of advanced sunnypilot controls.<br>This only changes the visibility of the toggles; " +
"it does not change the actual enabled/disabled state."), param="ShowAdvancedControls")
self.enable_github_runner_toggle = toggle_item_sp(tr("GitHub Runner Service"), tr("Enables or disables the GitHub runner service."),
param="EnableGithubRunner")
self.enable_copyparty_toggle = toggle_item_sp(tr("copyparty Service"),
tr("copyparty is a very capable file server, you can use it to download your routes, view your logs " +
"and even make some edits on some files from your browser. " +
"Requires you to connect to your comma locally via its IP address."), param="EnableCopyparty")
self.prebuilt_toggle = toggle_item_sp(tr("Quickboot Mode"), "", param="QuickBootToggle", callback=self._on_prebuilt_toggled)
self.error_log_btn = button_item(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."), callback=self._on_error_log_clicked)
self.items: list = [self.show_advanced_controls, self.enable_github_runner_toggle, self.enable_copyparty_toggle, self.prebuilt_toggle, self.error_log_btn,]
@staticmethod
def _on_prebuilt_toggled(state):
if state:
Path(PREBUILT_PATH).touch(exist_ok=True)
else:
os.remove(PREBUILT_PATH)
ui_state.params.put_bool("QuickBootToggle", state)
def _on_delete_confirm(self, result):
if result == DialogResult.CONFIRM:
if os.path.exists(self.error_log_path):
os.remove(self.error_log_path)
def _on_error_log_closed(self, result, log_exists):
if result == DialogResult.CONFIRM and log_exists:
dialog2 = ConfirmDialog(tr("Would you like to delete this log?"), tr("Yes"), tr("No"), rich=False)
gui_app.set_modal_overlay(dialog2, callback=self._on_delete_confirm)
def _on_error_log_clicked(self):
text = ""
if os.path.exists(self.error_log_path):
text = f"<b>{datetime.datetime.fromtimestamp(os.path.getmtime(self.error_log_path)).strftime('%d-%b-%Y %H:%M:%S').upper()}</b><br><br>"
try:
with open(self.error_log_path) as file:
text += file.read()
except Exception:
pass
dialog = HtmlModalSP(text=text, callback=lambda result: self._on_error_log_closed(result, os.path.exists(self.error_log_path)))
gui_app.set_modal_overlay(dialog)
def _update_state(self):
disable_updates = ui_state.params.get_bool("DisableUpdates")
show_advanced = ui_state.params.get_bool("ShowAdvancedControls")
if (prebuilt_file := os.path.exists(PREBUILT_PATH)) != ui_state.params.get_bool("QuickBootToggle"):
ui_state.params.put_bool("QuickBootToggle", prebuilt_file)
self.prebuilt_toggle.action_item.set_state(prebuilt_file)
self.prebuilt_toggle.set_visible(show_advanced and not (self._is_release_branch or self._is_development_branch))
self.prebuilt_toggle.action_item.set_enabled(disable_updates)
if disable_updates:
self.prebuilt_toggle.set_description(tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it " +
"removes the prebuilt file so compilation of locally edited cpp files can be made."))
else:
self.prebuilt_toggle.set_description(tr("Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first."))
self.enable_copyparty_toggle.set_visible(show_advanced)
self.enable_github_runner_toggle.set_visible(show_advanced and not self._is_release_branch)
self.error_log_btn.set_visible(not self._is_release_branch)

View File

@@ -21,7 +21,8 @@ from openpilot.system.ui.widgets.toggle import ON_COLOR
from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP, ListItemSP, toggle_item_sp, option_item_sp
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
@@ -29,11 +30,6 @@ if gui_app.sunnypilot_ui():
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
class ModelAction(ButtonActionSP):
def get_width_hint(self):
return super().get_width_hint() + 1
class ModelsLayout(Widget):
def __init__(self):
super().__init__()
@@ -55,7 +51,7 @@ class ModelsLayout(Widget):
self.current_model_item = ListItemSP(
title=tr("Current Model"),
description="",
action_item=ModelAction(tr("SELECT")),
action_item=NoElideButtonAction(tr("SELECT")),
callback=self._handle_current_model_clicked
)
@@ -70,7 +66,7 @@ class ModelsLayout(Widget):
self.clear_cache_item = ListItemSP(
title=tr("Clear Model Cache"),
description="",
action_item=ModelAction(tr("CLEAR")),
action_item=NoElideButtonAction(tr("CLEAR")),
callback=self._clear_cache
)

View File

@@ -4,27 +4,229 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import datetime
import os
import platform
import requests
import shutil
import threading
from pathlib import Path
from time import monotonic
from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.layouts.settings.software import time_ago
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import text_item
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeFolder, TreeNode, TreeOptionDialog
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
MAP_PATH = Path(Paths.mapd_root()) / "offline"
class OSMLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
self._current_percent = 0
self._last_map_size_update = 0
self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params
self._initialize_items()
self._update_map_size()
self._progress.set_visible(False)
self._state_btn.set_visible(False)
self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...")
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
self._mapd_version = text_item(tr("Mapd Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...")
self._delete_maps_btn = ListItemSP(tr("Downloaded Maps"), action_item=NoElideButtonAction(tr("DELETE"), enabled=True), callback=self._delete_maps)
self._progress = progress_item(tr("Downloading Map"))
self._update_btn = ListItemSP(tr("Database Update"), action_item=NoElideButtonAction(tr("CHECK"), enabled=True), callback=self._update_db)
self._country_btn = ListItemSP(tr("Country"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("Country"))
self._state_btn = ListItemSP(tr("State"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("State"))
]
return items
self.items = [self._mapd_version, self._delete_maps_btn, self._progress, self._update_btn, self._country_btn, self._state_btn]
def _render(self, rect):
self._scroller.render(rect)
def _show_confirm(self, msg, confirm_text, func):
gui_app.set_modal_overlay(ConfirmDialog(msg, confirm_text), lambda res: func() if res == DialogResult.CONFIRM else None)
def calculate_size(self):
total_size = 0
directories_to_scan = [MAP_PATH] if MAP_PATH.exists() else []
while directories_to_scan:
try:
for entry in os.scandir(directories_to_scan.pop()):
if entry.is_file():
total_size += entry.stat().st_size
elif entry.is_dir():
directories_to_scan.append(entry.path)
except OSError:
pass
self._delete_maps_btn.action_item.set_value(f"{total_size / 1024 ** 2:.2f} MB" if total_size < 1024 ** 3 else f"{total_size / 1024 ** 3:.2f} GB")
def _update_map_size(self):
threading.Thread(target=self.calculate_size, daemon=True).start()
def _do_delete_maps(self):
if MAP_PATH.exists():
shutil.rmtree(MAP_PATH)
for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"):
ui_state.params.remove(param)
self._delete_maps_btn.action_item.set_enabled(True)
self._delete_maps_btn.action_item.set_text(tr("DELETE"))
self._update_map_size()
def _on_confirm_delete_maps(self):
self._delete_maps_btn.action_item.set_enabled(False)
self._delete_maps_btn.action_item.set_text("DELETING...")
threading.Thread(target=self._do_delete_maps).start()
def _delete_maps(self):
self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
tr("Yes, delete all maps"), self._on_confirm_delete_maps)
def _update_db(self):
self._show_confirm(tr("This will start the download process and it might take a while to complete."), tr("Start Download"),
lambda: ui_state.params.put_bool("OsmDbUpdatesCheck", True))
def _select_region(self, region_type):
is_country = region_type == "Country"
btn = self._country_btn if is_country else self._state_btn
btn.action_item.set_enabled(False)
btn.action_item.set_text(tr("FETCHING..."))
threading.Thread(target=self._do_select_region, args=(region_type, btn)).start()
def _handle_region_selection(self, region_type, locations, key, res, ref):
if res != DialogResult.CONFIRM or not ref:
if region_type == "State" and res == DialogResult.CANCEL:
if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"):
ui_state.params.remove("OsmLocationName")
ui_state.params.remove("OsmLocationTitle")
ui_state.params.remove("OsmLocal")
self._update_labels()
return
if region_type == "Country":
ui_state.params.put_bool("OsmLocal", True)
ui_state.params.remove("OsmStateName")
ui_state.params.remove("OsmStateTitle")
ui_state.params.put(f"{key}Name", ref)
name = next((n.data['display_name'] for n in locations if n.ref == ref), ref)
ui_state.params.put(f"{key}Title", name)
if ref == "US" and region_type == "Country":
self._select_region("State")
else:
self._update_db()
def _do_select_region(self, region_type, btn):
base_url = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/"
url = base_url + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json")
try:
data = requests.get(url, timeout=10).json()
locations = sorted([TreeNode(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()], key=lambda n: n.data['display_name'])
except Exception:
locations = []
if region_type == "State":
locations.insert(0, TreeNode(ref="All", data={'display_name': tr("All states (~6.0 GB)")}))
btn.action_item.set_enabled(True)
btn.action_item.set_text(tr("SELECT"))
key = "OsmLocation" if region_type == "Country" else "OsmState"
current = ui_state.params.get(f"{key}Name") or ""
dialog = TreeOptionDialog(tr(f"Select {region_type}"), [TreeFolder(folder="", nodes=locations)], current_ref=current, search_prompt="Perform a search")
dialog.on_exit = lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref)
gui_app.set_modal_overlay(dialog, callback=lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref))
def _update_labels(self):
downloading = bool(self._mem_params.get("OSMDownloadLocations"))
self._country_btn.set_enabled(not downloading)
self._state_btn.set_enabled(not downloading)
self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US")
self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName")))
self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "")
self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "")
pending = ui_state.params.get_bool("OsmDbUpdatesCheck")
if downloading or pending:
if downloading:
device.reset_interactive_timeout()
self._update_map_size()
self._progress.set_visible(True)
progress = ui_state.params.get("OSMDownloadProgress")
total = progress.get('total_files', 0) if progress else 0
done = progress.get('downloaded_files', 0) if progress else 0
failed = total > 0 and not downloading and done < total
if total > 0:
progress_perc = max(0.0, min(100.0, (done / total) * 100.0))
else:
progress_perc = 0.0
if failed:
text = "0% - Downloading Maps"
btn_text = tr("Error: Invalid download. Retry.")
self._current_percent = 0.0
elif total > 0 and downloading:
self._current_percent = progress_perc
perc_int = int(progress_perc)
text = f"{perc_int}% - Downloading Maps"
btn_text = f"{done}/{total} ({perc_int}%)"
else:
self._current_percent = 0.0
text = "0% - Downloading Maps"
btn_text = tr("Downloading Maps...")
self._progress.action_item.update(self._current_percent, text, show_progress=total > 0 and downloading and not failed)
self._update_btn.action_item.set_enabled(not downloading) # TODO-SP: introduce CANCEL database download with mapd
self._update_btn.action_item.set_value(btn_text)
self._country_btn.action_item.set_enabled(not downloading)
self._state_btn.action_item.set_enabled(not downloading)
self._delete_maps_btn.action_item.set_enabled(not downloading)
else:
self._progress.set_visible(False)
self._update_btn.action_item.set_enabled(True)
self._country_btn.action_item.set_enabled(True)
self._state_btn.action_item.set_enabled(True)
self._delete_maps_btn.action_item.set_enabled(True)
ts = ui_state.params.get("OsmDownloadedDate")
dt: datetime.datetime | None = None
if ts:
try:
ts_f = float(ts)
if ts_f > 0:
dt = datetime.datetime.fromtimestamp(ts_f, tz=datetime.UTC)
except (ValueError, TypeError):
dt = None
formatted = time_ago(dt)
self._update_btn.action_item.set_value(tr("Last checked {}").format(formatted))
def show_event(self):
self._scroller.show_event()
def _update_state(self):
now = monotonic()
if now - self._last_map_size_update >= 1.0:
self._last_map_size_update = now
self._update_labels()
def _render(self, rect):
self._scroller.render(rect)

View File

@@ -9,7 +9,6 @@ from enum import IntEnum
import pyray as rl
from openpilot.selfdrive.ui.layouts.settings import settings as OP
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
@@ -31,6 +30,7 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import Steering
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
@@ -125,7 +125,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"),
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"),
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayoutSP(), icon="icons/shell.png"),
}
def _draw_sidebar(self, rect: rl.Rectangle):

View File

@@ -2,3 +2,8 @@ test
test_translations
test_ui/report_1
test_ui/raylib_report
diff/*.mp4
diff/*.html
diff/.coverage
diff/htmlcov/

201
selfdrive/ui/tests/diff/diff.py Executable file
View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python3
import os
import sys
import subprocess
import tempfile
import base64
import webbrowser
import argparse
from pathlib import Path
from openpilot.common.basedir import BASEDIR
DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report"
def extract_frames(video_path, output_dir):
output_pattern = str(output_dir / "frame_%04d.png")
cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y']
subprocess.run(cmd, capture_output=True, check=True)
frames = sorted(output_dir.glob("frame_*.png"))
return frames
def compare_frames(frame1_path, frame2_path):
result = subprocess.run(['cmp', '-s', frame1_path, frame2_path])
return result.returncode == 0
def frame_to_data_url(frame_path):
with open(frame_path, 'rb') as f:
data = f.read()
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
def create_diff_video(video1, video2, output_path):
"""Create a diff video using ffmpeg blend filter with difference mode."""
print("Creating diff video...")
cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path]
subprocess.run(cmd, capture_output=True, check=True)
def find_differences(video1, video2):
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
print(f"Extracting frames from {video1}...")
frames1_dir = tmpdir / "frames1"
frames1_dir.mkdir()
frames1 = extract_frames(video1, frames1_dir)
print(f"Extracting frames from {video2}...")
frames2_dir = tmpdir / "frames2"
frames2_dir.mkdir()
frames2 = extract_frames(video2, frames2_dir)
if len(frames1) != len(frames2):
print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}")
min_frames = min(len(frames1), len(frames2))
frames1 = frames1[:min_frames]
frames2 = frames2[:min_frames]
print(f"Comparing {len(frames1)} frames...")
different_frames = []
frame_data = []
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
is_different = not compare_frames(f1, f2)
if is_different:
different_frames.append(i)
if i < 10 or i >= len(frames1) - 10 or is_different:
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
return different_frames, frame_data, len(frames1)
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
chunks = []
if different_frames:
current_chunk = [different_frames[0]]
for i in range(1, len(different_frames)):
if different_frames[i] == different_frames[i - 1] + 1:
current_chunk.append(different_frames[i])
else:
chunks.append(current_chunk)
current_chunk = [different_frames[i]]
chunks.append(current_chunk)
result_text = (
f"✅ Videos are identical! ({total_frames} frames)"
if len(different_frames) == 0
else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)"
)
html = f"""<h2>UI Diff</h2>
<table>
<tr>
<td width='33%'>
<p><strong>Video 1</strong></p>
<video id='video1' width='100%' autoplay muted loop onplay='syncVideos()'>
<source src='{os.path.join(basedir, os.path.basename(video1))}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
<td width='33%'>
<p><strong>Video 2</strong></p>
<video id='video2' width='100%' autoplay muted loop onplay='syncVideos()'>
<source src='{os.path.join(basedir, os.path.basename(video2))}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
<td width='33%'>
<p><strong>Pixel Diff</strong></p>
<video id='diffVideo' width='100%' autoplay muted loop>
<source src='{os.path.join(basedir, 'diff.mp4')}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
</tr>
</table>
<script>
function syncVideos() {{
const video1 = document.getElementById('video1');
const video2 = document.getElementById('video2');
const diffVideo = document.getElementById('diffVideo');
video1.currentTime = video2.currentTime = diffVideo.currentTime;
}}
video1.addEventListener('timeupdate', () => {{
if (Math.abs(video1.currentTime - video2.currentTime) > 0.1) {{
video2.currentTime = video1.currentTime;
}}
if (Math.abs(video1.currentTime - diffVideo.currentTime) > 0.1) {{
diffVideo.currentTime = video1.currentTime;
}}
}});
video2.addEventListener('timeupdate', () => {{
if (Math.abs(video2.currentTime - video1.currentTime) > 0.1) {{
video1.currentTime = video2.currentTime;
}}
if (Math.abs(video2.currentTime - diffVideo.currentTime) > 0.1) {{
diffVideo.currentTime = video2.currentTime;
}}
}});
diffVideo.addEventListener('timeupdate', () => {{
if (Math.abs(diffVideo.currentTime - video1.currentTime) > 0.1) {{
video1.currentTime = diffVideo.currentTime;
video2.currentTime = diffVideo.currentTime;
}}
}});
</script>
<hr>
<p><strong>Results:</strong> {result_text}</p>
"""
return html
def main():
parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report')
parser.add_argument('video1', help='First video file')
parser.add_argument('video2', help='Second video file')
parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)')
parser.add_argument("--basedir", type=str, help="Base directory for output", default="")
parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser')
args = parser.parse_args()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
print("=" * 60)
print("VIDEO DIFF - HTML REPORT")
print("=" * 60)
print(f"Video 1: {args.video1}")
print(f"Video 2: {args.video2}")
print(f"Output: {args.output}")
print()
# Create diff video
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
create_diff_video(args.video1, args.video2, diff_video_path)
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
if different_frames is None:
sys.exit(1)
print()
print("Generating HTML report...")
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames)
with open(DIFF_OUT_DIR / args.output, 'w') as f:
f.write(html)
# Open in browser by default
if not args.no_open:
print(f"Opening {args.output} in browser...")
webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}')
return 0 if len(different_frames) == 0 else 1
if __name__ == "__main__":
sys.exit(main())

128
selfdrive/ui/tests/diff/replay.py Executable file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import os
import time
import coverage
import pyray as rl
from dataclasses import dataclass
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
os.environ["RECORD"] = "1"
if "RECORD_OUTPUT" not in os.environ:
os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4"
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
from openpilot.common.params import Params
from openpilot.system.version import terms_version, training_version
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
FPS = 60
HEADLESS = os.getenv("WINDOWED", "0") == "1"
@dataclass
class DummyEvent:
click: bool = False
# TODO: add some kind of intensity
swipe_left: bool = False
swipe_right: bool = False
swipe_down: bool = False
SCRIPT = [
(0, DummyEvent()),
(FPS * 1, DummyEvent(click=True)),
(FPS * 2, DummyEvent(click=True)),
(FPS * 3, DummyEvent()),
]
def setup_state():
params = Params()
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
params.put("DongleId", "test123456789")
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
return None
def inject_click(coords):
events = []
x, y = coords[0]
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=time.monotonic()))
for x, y in coords[1:]:
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=time.monotonic()))
x, y = coords[-1]
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic()))
with gui_app._mouse._lock:
gui_app._mouse._events.extend(events)
def handle_event(event: DummyEvent):
if event.click:
inject_click([(gui_app.width // 2, gui_app.height // 2)])
if event.swipe_left:
inject_click([(gui_app.width * 3 // 4, gui_app.height // 2),
(gui_app.width // 4, gui_app.height // 2),
(0, gui_app.height // 2)])
if event.swipe_right:
inject_click([(gui_app.width // 4, gui_app.height // 2),
(gui_app.width * 3 // 4, gui_app.height // 2),
(gui_app.width, gui_app.height // 2)])
if event.swipe_down:
inject_click([(gui_app.width // 2, gui_app.height // 4),
(gui_app.width // 2, gui_app.height * 3 // 4),
(gui_app.width // 2, gui_app.height)])
def run_replay():
setup_state()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
if not HEADLESS:
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
gui_app.init_window("ui diff test", fps=FPS)
main_layout = MiciMainLayout()
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
frame = 0
script_index = 0
for should_render in gui_app.render():
while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame:
_, event = SCRIPT[script_index]
handle_event(event)
script_index += 1
ui_state.update()
if should_render:
main_layout.render()
frame += 1
if script_index >= len(SCRIPT):
break
gui_app.close()
print(f"Total frames: {frame}")
print(f"Video saved to: {os.environ['RECORD_OUTPUT']}")
def main():
cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici'])
with cov.collect():
run_replay()
cov.stop()
cov.save()
cov.report()
cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov'))
print("HTML report: htmlcov/index.html")
if __name__ == "__main__":
main()

View File

@@ -88,9 +88,9 @@ if __name__ == "__main__":
print("Running...")
patch_submaster(message_chunks)
W, H = 1928, 1208
W, H = 2048, 1216
vipc = VisionIpcServer("camerad")
vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, 1928, 1208)
vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H)
vipc.start_listener()
yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2
yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes()

View File

@@ -68,10 +68,10 @@ msgid ""
"control alpha. Changing this setting will restart openpilot if the car is "
"powered on."
msgstr ""
"<b>경고: 이 차량에서 openpilot의 종방향 제어는 알파 버전이며 자동 긴급 제동"
"(AEB)을 비활성화합니다.</b><br><br>이 차량에서는 openpilot 종방향 제어 대신 "
"차량 내장 ACC가 기본으로 사용됩니다. openpilot 종방향 제어로 전환하려면 이 설"
"정을 켜세요. 종방향 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전"
"<b>경고: 이 차량에서 openpilot의 롱컨 제어는 알파 버전이며 자동 긴급 제동"
"(AEB)을 비활성화합니다.</b><br><br>이 차량에서는 openpilot 롱컨 제어 대신 "
"차량 내장 ACC가 기본으로 사용됩니다. openpilot 롱컨 제어로 전환하려면 이 설"
"정을 켜세요. 롱컨 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전"
"원이 켜져 있는 경우 이 설정을 변경하면 openpilot이 재시작됩니다."
#: selfdrive/ui/layouts/settings/device.py:148
@@ -130,7 +130,7 @@ msgstr "동의"
#: selfdrive/ui/layouts/settings/toggles.py:70
#, python-format
msgid "Always-On Driver Monitoring"
msgstr "항상 켜짐 운전자 모니터링"
msgstr "운전자 모니터링 항상 켜짐"
#: selfdrive/ui/layouts/settings/toggles.py:186
#, python-format
@@ -138,7 +138,7 @@ msgid ""
"An alpha version of openpilot longitudinal control can be tested, along with "
"Experimental mode, on non-release branches."
msgstr ""
"openpilot 종방향 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트"
"openpilot 롱컨 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트"
"할 수 있습니다."
#: selfdrive/ui/layouts/settings/device.py:187
@@ -192,7 +192,7 @@ msgstr "확인"
#: selfdrive/ui/widgets/exp_mode_button.py:50
#, python-format
msgid "CHILL MODE ON"
msgstr " 모드 켜짐"
msgstr "안정적 모드 켜짐"
#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73
#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136
@@ -283,7 +283,7 @@ msgstr "해제 후 재시작"
#: selfdrive/ui/layouts/settings/device.py:103
#, python-format
msgid "Disengage to Reset Calibration"
msgstr "해제 후 보정 재설정"
msgstr "해제 후 캘리브레이션 재설정"
#: selfdrive/ui/layouts/settings/toggles.py:32
msgid "Display speed in km/h instead of mph."
@@ -372,7 +372,7 @@ msgstr "openpilot 사용"
msgid ""
"Enable the openpilot longitudinal control (alpha) toggle to allow "
"Experimental mode."
msgstr "실험 모드를 사용하려면 openpilot 종방향 제어(알파) 토글을 켜세요."
msgstr "실험 모드를 사용하려면 openpilot 롱컨 제어(알파) 토글을 켜세요."
#: system/ui/widgets/network.py:204
#, python-format
@@ -415,7 +415,7 @@ msgid ""
"Experimental mode is currently unavailable on this car since the car's stock "
"ACC is used for longitudinal control."
msgstr ""
"이 차량은 종방향 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습"
"이 차량은 롱컨 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습"
"니다."
#: system/ui/widgets/network.py:373
@@ -430,11 +430,11 @@ msgstr "설정 완료"
#: selfdrive/ui/layouts/settings/settings.py:66
msgid "Firehose"
msgstr "Firehose"
msgstr "파이어호스"
#: selfdrive/ui/layouts/settings/firehose.py:18
msgid "Firehose Mode"
msgstr "Firehose 모드"
msgstr "파이어호스 모드"
#: selfdrive/ui/layouts/settings/firehose.py:25
msgid ""
@@ -462,7 +462,7 @@ msgstr ""
"최대의 효과를 위해 주 1회는 장치를 실내로 가져와 품질 좋은 USBC 어댑터와 "
"WiFi에 연결하세요.\n"
"\n"
"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 Firehose 모드가 동작합니"
"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 파이어호스 모드가 동작합니"
"다.\n"
"\n"
"\n"
@@ -470,7 +470,7 @@ msgstr ""
"\n"
"어떻게, 어디서 운전하는지가 중요한가요? 아니요. 평소처럼 운전하세요.\n"
"\n"
"Firehose 모드에서 모든 세그먼트가 가져가지나요? 아니요. 일부 세그먼트만 선택"
"파이어호스 모드에서 모든 구간을 가져가지나요? 아니요. 일부 구간만 선택"
"적으로 가져갑니다.\n"
"\n"
"좋은 USBC 어댑터는 무엇인가요? 빠른 휴대폰 또는 노트북 충전기면 충분합니"
@@ -544,7 +544,7 @@ msgstr "LTE"
#: selfdrive/ui/layouts/settings/developer.py:64
#, python-format
msgid "Longitudinal Maneuver Mode"
msgstr "종방향 매뉴버 모드"
msgstr "롱컨 기동 모드"
#: selfdrive/ui/onroad/hud_renderer.py:148
#, python-format
@@ -623,7 +623,7 @@ msgstr "미리보기"
#: selfdrive/ui/widgets/prime.py:44
#, python-format
msgid "PRIME FEATURES:"
msgstr "prime 기능:"
msgstr "프라임 기능:"
#: selfdrive/ui/layouts/settings/device.py:48
#, python-format
@@ -646,7 +646,7 @@ msgid ""
"Pair your device with comma connect (connect.comma.ai) and claim your comma "
"prime offer."
msgstr ""
"장치를 comma connect(connect.comma.ai)와 페어링하고 comma prime 혜택을 받으세"
"장치를 comma connect(connect.comma.ai)와 페어링하고 comma 프라임 혜택을 받으세"
"요."
#: selfdrive/ui/widgets/setup.py:91
@@ -748,7 +748,7 @@ msgstr "규제 정보"
#: selfdrive/ui/layouts/settings/toggles.py:98
#, python-format
msgid "Relaxed"
msgstr "편안"
msgstr "편안"
#: selfdrive/ui/widgets/prime.py:47
#, python-format
@@ -773,7 +773,7 @@ msgstr "재설정"
#: selfdrive/ui/layouts/settings/device.py:51
#, python-format
msgid "Reset Calibration"
msgstr "보정 재설정"
msgstr "캘리브레이션 재설정"
#: selfdrive/ui/layouts/settings/device.py:65
#, python-format
@@ -841,7 +841,7 @@ msgid ""
"cycle through these personalities with your steering wheel distance button."
msgstr ""
"표준을 권장합니다. 공격적 모드에서는 앞차를 더 가깝게 따라가고 가감속이 더 적"
"극적입니다. 편안 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어"
"극적입니다. 편안 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어"
"링의 차간 버튼으로 이 성향들을 전환할 수 있습니다."
#: selfdrive/ui/onroad/alert_renderer.py:59
@@ -892,7 +892,7 @@ msgstr "제거"
#: selfdrive/ui/layouts/sidebar.py:117
msgid "Unknown"
msgstr "알없음"
msgstr "알없음"
#: selfdrive/ui/layouts/settings/software.py:48
#, python-format
@@ -994,7 +994,7 @@ msgstr "카메라 시작 중"
#: selfdrive/ui/widgets/prime.py:63
#, python-format
msgid "comma prime"
msgstr "comma prime"
msgstr "comma 프라임"
#: system/ui/widgets/network.py:142
#, python-format
@@ -1054,7 +1054,7 @@ msgstr "지금"
#: selfdrive/ui/layouts/settings/developer.py:71
#, python-format
msgid "openpilot Longitudinal Control (Alpha)"
msgstr "openpilot 종방향 제어(알파)"
msgstr "openpilot 롱컨 제어(알파)"
#: selfdrive/ui/onroad/alert_renderer.py:51
#, python-format
@@ -1076,9 +1076,9 @@ msgid ""
"some turns. The Experimental mode logo will also be shown in the top right "
"corner."
msgstr ""
"openpilot은 기본적으로 모드로 주행합니다. 실험 모드를 사용하면 모드에 "
"openpilot은 기본적으로 안정적 모드로 주행합니다. 실험 모드를 사용하면 안정적 모드에 "
"아직 준비되지 않은 알파 수준의 기능이 활성화됩니다. 실험 기능은 아래와 같습니"
"다:<br><h4>엔드투엔드 종방향 제어</h4><br>주행 모델이 가속과 제동을 제어합니"
"다:<br><h4>엔드투엔드 롱컨 제어</h4><br>주행 모델이 가속과 제동을 제어합니"
"다. openpilot은 빨간 신호 및 정지 표지에서의 정지를 포함해 사람이 운전한다고 "
"판단하는 방식으로 주행합니다. 주행 속도는 모델이 결정하므로 설정 속도는 상한"
"으로만 동작합니다. 알파 품질 기능이므로 오작동이 발생할 수 있습니다.<br><h4>"
@@ -1111,7 +1111,7 @@ msgstr ""
#: selfdrive/ui/layouts/settings/toggles.py:183
#, python-format
msgid "openpilot longitudinal control may come in a future update."
msgstr "openpilot 종방향 제어는 향후 업데이트에서 제공될 수 있습니다."
msgstr "openpilot 롱컨 제어는 향후 업데이트에서 제공될 수 있습니다."
#: selfdrive/ui/layouts/settings/device.py:26
msgid ""
@@ -1177,7 +1177,7 @@ msgstr[0] "{}분 전"
#, python-format
msgid "{} segment of your driving is in the training dataset so far."
msgid_plural "{} segments of your driving is in the training dataset so far."
msgstr[0] "현재까지 귀하의 주행 {}세그먼트가 학습 데이터셋에 포함되었습니다."
msgstr[0] "현재까지 귀하의 주행 {}구간이 학습 데이터셋에 포함되었습니다."
#: selfdrive/ui/widgets/prime.py:62
#, python-format
@@ -1187,4 +1187,4 @@ msgstr "✓ 구독됨"
#: selfdrive/ui/widgets/setup.py:22
#, python-format
msgid "🔥 Firehose Mode 🔥"
msgstr "🔥 Firehose 모드 🔥"
msgstr "🔥 파이어호스 모드 🔥"

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"Português": "pt-BR",
"Español": "es",
"Türkçe": "tr",
"Українська": "uk",
"العربية": "ar",
"ไทย": "th",
"中文(繁體)": "zh-CHT",

View File

@@ -64,12 +64,7 @@ def request_refresh_osm_location_data(nations: list[str], states: list[str] = No
"states": states or []
}
osm_download_locations_dump = json.dumps({
"nations": nations,
"states": states or []
})
print(f"Downloading maps for {osm_download_locations_dump}")
print(f"Downloading maps for {json.dumps(osm_download_locations)}")
mem_params.put("OSMDownloadLocations", osm_download_locations)
@@ -103,8 +98,6 @@ def filter_nations_and_states(nations: list[str], states: list[str] = None) -> t
def update_osm_db() -> None:
# last_downloaded_date = params.get("OsmDownloadedDate", return_default=True)
# if params.get_bool("OsmDbUpdatesCheck") or time.monotonic() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60
if params.get_bool("OsmDbUpdatesCheck"):
cleanup_old_osm_data(get_files_for_cleanup())
country = params.get("OsmLocationName", return_default=True)

View File

@@ -11,13 +11,12 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
ConfidenceClass = log.ModelDataV2.ConfidenceClass
def get_curvature_from_output(output, vego, lat_action_t, mlsim):
def get_curvature_from_output(output, plan, vego, lat_action_t, mlsim):
if not mlsim:
if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly
return float(desired_curv[0, 0])
plan_output = output['plan'][0]
return float(get_curvature_from_plan(plan_output[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan_output[:, Plan.ORIENTATION_RATE][:, 2],
return float(get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan[:, Plan.ORIENTATION_RATE][:, 2],
ModelConstants.T_IDXS, vego, lat_action_t))

View File

@@ -28,6 +28,7 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
RECOVERY_POWER = 1.0 # The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong
class FrameMeta:
@@ -156,11 +157,13 @@ class ModelState(ModelStateBase):
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
plan = model_output['plan'][0]
if 'planplus' in model_output:
plan = plan + RECOVERY_POWER*model_output['planplus'][0]
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
action_t=long_action_t)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim)
desired_curvature = get_curvature_from_output(model_output, plan, v_ego, lat_action_t, self.mlsim)
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
if v_ego > self.MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)

View File

@@ -108,6 +108,8 @@ class Parser:
plan_in_N, plan_out_N = (SplitModelConstants.PLAN_MHP_N, SplitModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N,
out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH))
if 'planplus' in outs:
self.parse_mdn('planplus', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH))
def split_outputs(self, outs: dict[str, np.ndarray]) -> None:
if 'desired_curvature' in outs:

View File

@@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths
from pathlib import Path
# see the README.md for more details on the model selector versioning
CURRENT_SELECTOR_VERSION = 12
CURRENT_SELECTOR_VERSION = 13
REQUIRED_MIN_SELECTOR_VERSION = 12
USE_ONNX = os.getenv('USE_ONNX', PC)

View File

@@ -1 +1 @@
030a2a502e95e51290bb1d76795013b72b25521a572c3942a232b9395e544250
6168bc755ea17aececa535e8b94e6c798e5e855bfc47be19220d5cbc08483332

View File

@@ -0,0 +1,112 @@
"""
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import custom
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
# Acceleration Profiles
MAX_ACCEL_PROFILES = {
AccelPersonality.eco: [2.0, 1.99, 1.88, 1.10, 0.500, 0.292, 0.15, 0.10],
AccelPersonality.normal: [1.0, 2.00, 1.94, 1.22, 0.635, 0.33, 0.22, 0.16],
AccelPersonality.sport: [.5, 2.00, 2.00, 1.85, 0.800, 0.54, 0.32, 0.22],
}
MAX_ACCEL_BREAKPOINTS = [0., 4., 6., 9., 16., 25., 30., 55.]
# Braking Profiles
MIN_ACCEL_PROFILES = {
AccelPersonality.eco: [-0.14, -0.0006, -0.010, -0.30, -1.20],
AccelPersonality.normal: [-0.1, -0.0007, -0.012, -0.35, -1.20],
AccelPersonality.sport: [-0.6, -0.0008, -0.014, -0.40, -1.20],
}
MIN_ACCEL_BREAKPOINTS = [0., 3., 11., 14., 50.]
class AccelPersonalityController:
def __init__(self):
self.params = Params()
self.frame = 0
self.accel_personality = AccelPersonality.normal
self.param_keys = {
'personality': 'AccelPersonality',
'enabled': 'AccelPersonalityEnabled'
}
self._load_personality_from_params()
def _load_personality_from_params(self):
try:
saved = self.params.get(self.param_keys['personality'])
if saved is not None:
personality_value = int(saved)
if personality_value in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
self.accel_personality = personality_value
else:
cloudlog.warning(f"Invalid personality value {personality_value}, using normal")
self.accel_personality = AccelPersonality.normal
except (ValueError, TypeError) as e:
cloudlog.warning(f"Failed to load personality from params: {e}")
self.accel_personality = AccelPersonality.normal
def _update_from_params(self):
if self.frame % int(1. / DT_MDL) != 0:
return
self._load_personality_from_params()
def get_accel_personality(self) -> int:
self._update_from_params()
return int(self.accel_personality)
def set_accel_personality(self, personality: int):
if personality not in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
cloudlog.error(f"Invalid personality {personality}, ignoring")
return
self.accel_personality = personality
self.params.put(self.param_keys['personality'], str(personality))
cloudlog.info(f"Accel personality set to {personality}")
def cycle_accel_personality(self) -> int:
personalities = [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]
current_idx = personalities.index(self.accel_personality)
next_personality = personalities[(current_idx + 1) % len(personalities)]
self.set_accel_personality(next_personality)
return int(next_personality)
def get_accel_limits(self, v_ego: float) -> tuple[float, float]:
max_a = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self.accel_personality])
min_a = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[self.accel_personality])
return float(min_a), float(max_a)
def get_min_accel(self, v_ego: float) -> float:
return self.get_accel_limits(v_ego)[0]
def get_max_accel(self, v_ego: float) -> float:
return self.get_accel_limits(v_ego)[1]
def is_enabled(self) -> bool:
return bool(self.params.get_bool(self.param_keys['enabled']))
def set_enabled(self, enabled: bool):
self.params.put_bool(self.param_keys['enabled'], enabled)
cloudlog.info(f"Accel personality controller {'enabled' if enabled else 'disabled'}")
def toggle_enabled(self) -> bool:
current = self.is_enabled()
self.set_enabled(not current)
return not current
def reset(self):
self.accel_personality = AccelPersonality.normal
self.frame = 0
def update(self):
self.frame += 1
self._update_from_params()

View File

@@ -0,0 +1,286 @@
"""
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pytest
import numpy as np
from cereal import custom
from sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import (
AccelPersonalityController,
MAX_ACCEL_PROFILES,
MIN_ACCEL_PROFILES,
MAX_ACCEL_BREAKPOINTS,
MIN_ACCEL_BREAKPOINTS,
)
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
class TestAccelPersonalityController:
@pytest.fixture
def mock_params(self, mocker):
params = mocker.Mock()
params.get = mocker.Mock(return_value=None)
params.get_bool = mocker.Mock(return_value=False)
params.put = mocker.Mock()
params.put_bool = mocker.Mock()
return params
@pytest.fixture
def controller(self, mock_params, mocker):
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
ctrl = AccelPersonalityController()
ctrl.params = mock_params
return ctrl
def test_initialization_defaults(self, controller):
assert controller.frame == 0
assert controller.accel_personality == AccelPersonality.normal
assert controller.param_keys == {
'personality': 'AccelPersonality',
'enabled': 'AccelPersonalityEnabled'
}
@pytest.mark.parametrize("personality,expected", [
(AccelPersonality.eco, AccelPersonality.eco),
(AccelPersonality.normal, AccelPersonality.normal),
(AccelPersonality.sport, AccelPersonality.sport),
])
def test_load_personality_valid(self, mocker, personality, expected):
mock_params = mocker.Mock()
mock_params.get = mocker.Mock(return_value=str(personality).encode())
mock_params.get_bool = mocker.Mock(return_value=False)
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
controller = AccelPersonalityController()
assert controller.accel_personality == expected
def test_load_personality_invalid_value(self, mocker):
mock_params = mocker.Mock()
mock_params.get = mocker.Mock(return_value=b'999')
mock_params.get_bool = mocker.Mock(return_value=False)
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
controller = AccelPersonalityController()
assert controller.accel_personality == AccelPersonality.normal
def test_load_personality_parse_error(self, mocker):
mock_params = mocker.Mock()
mock_params.get = mocker.Mock(return_value=b'invalid_data')
mock_params.get_bool = mocker.Mock(return_value=False)
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
controller = AccelPersonalityController()
assert controller.accel_personality == AccelPersonality.normal
def test_load_personality_none(self, mocker):
mock_params = mocker.Mock()
mock_params.get = mocker.Mock(return_value=None)
mock_params.get_bool = mocker.Mock(return_value=False)
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
controller = AccelPersonalityController()
assert controller.accel_personality == AccelPersonality.normal
def test_get_accel_personality(self, controller):
controller.accel_personality = AccelPersonality.sport
result = controller.get_accel_personality()
assert result == AccelPersonality.sport
assert isinstance(result, int)
@pytest.mark.parametrize("personality", [
AccelPersonality.eco,
AccelPersonality.normal,
AccelPersonality.sport,
])
def test_set_accel_personality_valid(self, controller, mock_params, personality):
controller.set_accel_personality(personality)
assert controller.accel_personality == personality
mock_params.put.assert_called_once_with('AccelPersonality', str(personality))
def test_set_accel_personality_invalid(self, controller, mock_params):
original = controller.accel_personality
controller.set_accel_personality(999)
assert controller.accel_personality == original
mock_params.put.assert_not_called()
def test_cycle_accel_personality_full_cycle(self, controller):
controller.accel_personality = AccelPersonality.eco
result = controller.cycle_accel_personality()
assert result == AccelPersonality.normal
assert controller.accel_personality == AccelPersonality.normal
result = controller.cycle_accel_personality()
assert result == AccelPersonality.sport
assert controller.accel_personality == AccelPersonality.sport
result = controller.cycle_accel_personality()
assert result == AccelPersonality.eco
assert controller.accel_personality == AccelPersonality.eco
def test_cycle_accel_personality_return_type(self, controller):
result = controller.cycle_accel_personality()
assert isinstance(result, int)
@pytest.mark.parametrize("personality", [
AccelPersonality.eco,
AccelPersonality.normal,
AccelPersonality.sport,
])
def test_get_accel_limits_at_zero_speed(self, controller, personality):
controller.accel_personality = personality
min_a, max_a = controller.get_accel_limits(0.0)
expected_max = MAX_ACCEL_PROFILES[personality][0]
expected_min = MIN_ACCEL_PROFILES[personality][0]
assert abs(max_a - expected_max) < 1e-6
assert abs(min_a - expected_min) < 1e-6
@pytest.mark.parametrize("v_ego,personality", [
(0.0, AccelPersonality.eco),
(10.0, AccelPersonality.normal),
(25.0, AccelPersonality.sport),
(50.0, AccelPersonality.eco),
(4.0, AccelPersonality.normal),
(30.0, AccelPersonality.sport),
])
def test_get_accel_limits_interpolation(self, controller, v_ego, personality):
controller.accel_personality = personality
min_a, max_a = controller.get_accel_limits(v_ego)
expected_max = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[personality])
expected_min = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[personality])
assert abs(max_a - expected_max) < 1e-6
assert abs(min_a - expected_min) < 1e-6
def test_get_accel_limits_return_types(self, controller):
min_a, max_a = controller.get_accel_limits(10.0)
assert isinstance(min_a, float)
assert isinstance(max_a, float)
def test_get_min_accel(self, controller):
controller.accel_personality = AccelPersonality.sport
v_ego = 15.0
min_a = controller.get_min_accel(v_ego)
expected = controller.get_accel_limits(v_ego)[0]
assert min_a == expected
assert isinstance(min_a, float)
def test_get_max_accel(self, controller):
controller.accel_personality = AccelPersonality.eco
v_ego = 20.0
max_a = controller.get_max_accel(v_ego)
expected = controller.get_accel_limits(v_ego)[1]
assert max_a == expected
assert isinstance(max_a, float)
def test_is_enabled_true(self, controller, mock_params):
mock_params.get_bool.return_value = True
assert controller.is_enabled() is True
def test_is_enabled_false(self, controller, mock_params):
mock_params.get_bool.return_value = False
assert controller.is_enabled() is False
def test_is_enabled_calls_params(self, controller, mock_params):
controller.is_enabled()
mock_params.get_bool.assert_called_once_with('AccelPersonalityEnabled')
@pytest.mark.parametrize("enabled", [True, False])
def test_set_enabled(self, controller, mock_params, enabled):
controller.set_enabled(enabled)
mock_params.put_bool.assert_called_once_with('AccelPersonalityEnabled', enabled)
def test_toggle_enabled_from_false(self, controller, mock_params):
mock_params.get_bool.return_value = False
result = controller.toggle_enabled()
assert result is True
mock_params.put_bool.assert_called_once_with('AccelPersonalityEnabled', True)
def test_toggle_enabled_from_true(self, controller, mock_params):
mock_params.get_bool.return_value = True
result = controller.toggle_enabled()
assert result is False
mock_params.put_bool.assert_called_once_with('AccelPersonalityEnabled', False)
def test_reset(self, controller):
controller.accel_personality = AccelPersonality.sport
controller.frame = 100
controller.reset()
assert controller.accel_personality == AccelPersonality.normal
assert controller.frame == 0
def test_update_increments_frame(self, controller):
initial_frame = controller.frame
controller.update()
assert controller.frame == initial_frame + 1
def test_update_multiple_calls(self, controller):
for i in range(1, 11):
controller.update()
assert controller.frame == i
@pytest.mark.parametrize("v_ego", [0, 5, 10, 15, 20, 25, 30, 40, 50, 55])
@pytest.mark.parametrize("personality", [
AccelPersonality.eco,
AccelPersonality.normal,
AccelPersonality.sport,
])
def test_accel_limits_physical_constraints(self, controller, v_ego, personality):
controller.accel_personality = personality
min_a, max_a = controller.get_accel_limits(v_ego)
assert min_a < 0
assert max_a > 0
assert min_a < max_a
def test_max_accel_decreases_with_speed(self, controller):
test_speeds = [0, 10, 20, 30, 40, 50]
for personality in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
controller.accel_personality = personality
max_accels = [controller.get_max_accel(v) for v in test_speeds]
assert max_accels[-1] < max_accels[0]
@pytest.mark.parametrize("v_ego", [5, 10, 15, 20, 25])
def test_acceleration_personality_ordering(self, controller, v_ego):
controller.accel_personality = AccelPersonality.eco
_, eco_max = controller.get_accel_limits(v_ego)
controller.accel_personality = AccelPersonality.normal
_, normal_max = controller.get_accel_limits(v_ego)
controller.accel_personality = AccelPersonality.sport
_, sport_max = controller.get_accel_limits(v_ego)
assert sport_max >= normal_max
assert normal_max >= eco_max
@pytest.mark.parametrize("v_ego", [5, 10, 15, 20])
def test_braking_personality_ordering(self, controller, v_ego):
controller.accel_personality = AccelPersonality.eco
eco_min, _ = controller.get_accel_limits(v_ego)
controller.accel_personality = AccelPersonality.sport
sport_min, _ = controller.get_accel_limits(v_ego)
assert sport_min <= eco_min
def test_accel_limits_at_max_speed(self, controller):
max_speed = MAX_ACCEL_BREAKPOINTS[-1]
min_a, max_a = controller.get_accel_limits(max_speed)
assert isinstance(min_a, float)
assert isinstance(max_a, float)
def test_accel_limits_beyond_max_speed(self, controller):
beyond_max = MAX_ACCEL_BREAKPOINTS[-1] + 10
min_a, max_a = controller.get_accel_limits(beyond_max)
assert isinstance(min_a, float)
assert isinstance(max_a, float)
def test_accel_limits_very_low_speed(self, controller):
min_a, max_a = controller.get_accel_limits(0.5)
assert isinstance(min_a, float)
assert isinstance(max_a, float)
assert min_a >= -2.0

View File

@@ -15,9 +15,8 @@ LAT_PLAN_MIN_IDX = 5
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
# from selfdrive/controls/lib/latcontrol_torque.py
KP = 1.0
KI = 0.3
KD = 0.0
KP = 0.8
KI = 0.15
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]
@@ -64,7 +63,7 @@ class LatControlTorqueExtBase:
self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
self._ff = 0.0
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD)
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI)
self._pid_log = None
self._setpoint = 0.0
self._measurement = 0.0

View File

@@ -17,6 +17,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolve
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
@@ -26,6 +27,7 @@ class LongitudinalPlannerSP:
self.events_sp = EventsSP()
self.resolver = SpeedLimitResolver()
self.dec = DynamicExperimentalController(CP, mpc)
self.accel_controller = AccelPersonalityController()
self.scc = SmartCruiseControl()
self.resolver = SpeedLimitResolver()
self.sla = SpeedLimitAssist(CP, CP_SP)
@@ -81,6 +83,7 @@ class LongitudinalPlannerSP:
self.events_sp.clear()
self.dec.update(sm)
self.e2e_alerts_helper.update(sm, self.events_sp)
self.accel_controller.update()
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
plan_sp_send = messaging.new_message('longitudinalPlanSP')

View File

@@ -2,9 +2,10 @@ import json
import os
import random
import time
from datetime import datetime, timedelta
import jwt
from typing import cast
from datetime import datetime, timedelta, UTC
from openpilot.common.api.base import BaseApi
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
@@ -92,7 +93,8 @@ class SunnylinkApi(BaseApi):
backoff = 1
while True:
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm=jwt_algo)
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)},
cast(str, private_key), algorithm=jwt_algo)
try:
if verbose or time.monotonic() - start_time < timeout / 2:
self._status_update("Registering device to sunnylink...")

View File

@@ -1,4 +1,26 @@
{
"AccelPersonality": {
"title": "Acceleration Personality",
"description": "",
"options": [
{
"value": 0,
"label": "Sport"
},
{
"value": 1,
"label": "Normal"
},
{
"value": 2,
"label": "Eco"
}
]
},
"AccelPersonalityEnabled": {
"title": "Enable Acceleration Personality",
"description": "Controls acceleration behavior: Eco (efficient), Normal (balanced), Sport (responsive)."
},
"AccessToken": {
"title": "AccessTokenIsNice",
"description": ""
@@ -258,11 +280,11 @@
"description": ""
},
"EnableCopyparty": {
"title": "Enable Copyparty",
"title": "copyparty Service",
"description": ""
},
"EnableGithubRunner": {
"title": "Enable GitHub Runner",
"title": "GitHub Runner Service",
"description": ""
},
"EnableSunnylinkUploader": {
@@ -458,6 +480,10 @@
"title": "Language",
"description": ""
},
"LastAgnosPowerMonitorShutdown": {
"title": "Last AGNOS Power Monitor Shutdown",
"description": ""
},
"LastAthenaPingTime": {
"title": "Last Athena Ping Time",
"description": ""

View File

@@ -17,7 +17,7 @@ from cereal.messaging import SubMaster
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
from openpilot.common.utils import atomic_write_in_dir
from openpilot.common.utils import atomic_write
from openpilot.system.version import get_build_metadata
from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S
from openpilot.system.statsd import METRIC_TYPE, StatLogSP
@@ -242,7 +242,7 @@ def stats_main(end_event):
if len(os.listdir(STATS_DIR)) < STATS_DIR_FILE_LIMIT:
if len(result) > 0:
stats_path = os.path.join(STATS_DIR, f"{boot_uid}_{idx}")
with atomic_write_in_dir(stats_path) as f:
with atomic_write(stats_path) as f:
f.write(result)
idx += 1
else:

View File

@@ -31,7 +31,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
import cereal.messaging as messaging
from cereal import log
from cereal.services import SERVICE_LIST
from openpilot.common.api import Api
from openpilot.common.api import Api, get_key_pair
from openpilot.common.utils import CallbackReader, get_upload_stream
from openpilot.common.params import Params
from openpilot.common.realtime import set_core_affinity
@@ -554,11 +554,8 @@ def start_local_proxy_shim(global_end_event: threading.Event, local_port: int, w
@dispatcher.add_method
def getPublicKey() -> str | None:
if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'):
return None
with open(Paths.persist_root() + '/comma/id_rsa.pub') as f:
return f.read()
_, _, public_key = get_key_pair()
return public_key
@dispatcher.add_method

View File

@@ -2,6 +2,7 @@
import time
import json
import jwt
from typing import cast
from pathlib import Path
from datetime import datetime, timedelta, UTC
@@ -69,7 +70,9 @@ def register(show_spinner=False) -> str | None:
start_time = time.monotonic()
while True:
try:
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, private_key, algorithm=jwt_algo)
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)},
cast(str, private_key), algorithm=jwt_algo)
cloudlog.info("getting pilotauth")
cloudlog.info("getting pilotauth")
resp = api_get("v2/pilotauth/", method='POST', timeout=15,
imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token)

View File

@@ -1004,8 +1004,8 @@ bool SpectraCamera::openSensor() {
};
// Figure out which sensor we have
if (!init_sensor_lambda(new OX03C10) &&
!init_sensor_lambda(new OS04C10)) {
if (!init_sensor_lambda(new OS04C10) &&
!init_sensor_lambda(new OX03C10)) {
LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num);
enabled = false;
return false;

View File

@@ -1,6 +1,7 @@
#include <cmath>
#include "system/camerad/sensors/sensor.h"
#include "third_party/linux/include/msm_camsensor_sdk.h"
namespace {
@@ -51,7 +52,7 @@ OS04C10::OS04C10() {
probe_expected_data = 0x5304;
bits_per_pixel = 12;
mipi_format = CAM_FORMAT_MIPI_RAW_12;
frame_data_type = 0x2c;
frame_data_type = CSI_RAW12;
mclk_frequency = 24000000; // Hz
// TODO: this was set from logs. actually calculate it out

View File

@@ -4,10 +4,10 @@ const struct i2c_random_wr_payload start_reg_array_os04c10[] = {{0x100, 1}};
const struct i2c_random_wr_payload stop_reg_array_os04c10[] = {{0x100, 0}};
const struct i2c_random_wr_payload init_array_os04c10[] = {
// DP_2688X1520_NEWSTG_MIPI0776Mbps_30FPS_10BIT_FOURLANE
{0x0103, 0x01},
// baseed on DP_2688X1520_NEWSTG_MIPI0776Mbps_30FPS_10BIT_FOURLANE
{0x0103, 0x01}, // software reset
// PLL
// PLL + clocks
{0x0301, 0xe4},
{0x0303, 0x01},
{0x0305, 0xb6},
@@ -24,7 +24,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x3106, 0x21},
{0x3107, 0xa1},
// ?
// Analog/timing fine-tuning block
{0x3624, 0x00},
{0x3625, 0x4c},
{0x3660, 0x04},
@@ -101,7 +101,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x3f00, 0x0b},
{0x3f06, 0x04},
// BLC
// BLC - black level correction
{0x400a, 0x01},
{0x400b, 0x50},
{0x400e, 0x08},
@@ -157,7 +157,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x5180, 0x70},
{0x5181, 0x10},
// DPC
// DPC - defective pixel correction
{0x520a, 0x03},
{0x520b, 0x06},
{0x520c, 0x0c},
@@ -248,7 +248,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x4008, 0x01},
{0x4009, 0x06},
// FSIN
// FSIN - frame sync
{0x3002, 0x22},
{0x3663, 0x22},
{0x368a, 0x04},
@@ -276,8 +276,8 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x3816, 0x03},
{0x3817, 0x01},
{0x380c, 0x0b}, {0x380d, 0xac}, // HTS
{0x380e, 0x06}, {0x380f, 0x9c}, // VTS
{0x380c, 0x0b}, {0x380d, 0xac}, // HTS (line length)
{0x380e, 0x06}, {0x380f, 0x9c}, // VTS (frame length)
{0x3820, 0xb3},
{0x3821, 0x01},
@@ -309,17 +309,17 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
// initialize exposure
{0x3503, 0x88},
// long
// long exposure
{0x3500, 0x00}, {0x3501, 0x00}, {0x3502, 0x10},
{0x3508, 0x00}, {0x3509, 0x80},
{0x350a, 0x04}, {0x350b, 0x00},
// short
// short exposure
{0x3510, 0x00}, {0x3511, 0x00}, {0x3512, 0x40},
{0x350c, 0x00}, {0x350d, 0x80},
{0x350e, 0x04}, {0x350f, 0x00},
// wb
// white balance
// b
{0x5100, 0x06}, {0x5101, 0x7e},
{0x5140, 0x06}, {0x5141, 0x7e},
@@ -332,7 +332,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
};
const struct i2c_random_wr_payload ife_downscale_override_array_os04c10[] = {
// OS04C10_AA_00_02_17_wAO_2688x1524_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz
// based on OS04C10_AA_00_02_17_wAO_2688x1524_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz
{0x3c8c, 0x40},
{0x3714, 0x24},
{0x37c2, 0x04},

View File

@@ -1,6 +1,7 @@
#include <cmath>
#include "system/camerad/sensors/sensor.h"
#include "third_party/linux/include/msm_camsensor_sdk.h"
namespace {
@@ -40,8 +41,8 @@ OX03C10::OX03C10() {
probe_expected_data = 0x5803;
bits_per_pixel = 12;
mipi_format = CAM_FORMAT_MIPI_RAW_12;
frame_data_type = 0x2c; // one is 0x2a, two are 0x2b
mclk_frequency = 24000000; //Hz
frame_data_type = CSI_RAW12;
mclk_frequency = 24000000; // Hz
readout_time_ns = 14697000;

View File

@@ -7,7 +7,6 @@
#include <string>
#include <algorithm> // for std::clamp
#include "common/params.h"
#include "common/util.h"
#include "system/hardware/base.h"

View File

@@ -16,6 +16,7 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.timeout import Timeout
from openpilot.system.hardware.hw import Paths
from openpilot.system.hardware import TICI
from openpilot.system.loggerd.xattr_cache import getxattr
from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE
from openpilot.system.manager.process_config import managed_processes
@@ -221,13 +222,16 @@ class TestLoggerd:
assert abs(boot.wallTimeNanos - time.time_ns()) < 5*1e9 # within 5s
assert boot.launchLog == launch_log
for fn in ["console-ramoops", "pmsg-ramoops-0"]:
path = Path(os.path.join("/sys/fs/pstore/", fn))
if path.is_file():
with open(path, "rb") as f:
expected_val = f.read()
bootlog_val = [e.value for e in boot.pstore.entries if e.key == fn][0]
assert expected_val == bootlog_val
if TICI:
for fn in ["console-ramoops", "pmsg-ramoops-0"]:
path = Path(os.path.join("/sys/fs/pstore/", fn))
if path.is_file():
with open(path, "rb") as f:
expected_val = f.read()
bootlog_val = [e.value for e in boot.pstore.entries if e.key == fn][0]
assert expected_val == bootlog_val
else:
assert len(boot.pstore.entries) == 0
# next one should increment by one
bl1 = re.match(RE.LOG_ID_V2, bootlog_path.name)

View File

@@ -14,7 +14,7 @@ from openpilot.system.version import get_build_metadata
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
TOTAL_SCONS_NODES = 2280
TOTAL_SCONS_NODES = 2705
MAX_BUILD_PROGRESS = 100
def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:

View File

@@ -9,6 +9,7 @@ import traceback
from cereal import log
import cereal.messaging as messaging
import openpilot.system.sentry as sentry
from openpilot.common.utils import atomic_write
from openpilot.common.params import Params, ParamKeyFlag
from openpilot.common.text_window import TextWindow
from openpilot.system.hardware import HARDWARE
@@ -168,7 +169,7 @@ def manager_thread() -> None:
# kick AGNOS power monitoring watchdog
try:
if sm.all_checks(['deviceState']):
with open("/var/tmp/power_watchdog", "w") as f:
with atomic_write("/var/tmp/power_watchdog", "w", overwrite=True) as f:
f.write(str(time.monotonic()))
except Exception:
pass

View File

@@ -67,6 +67,7 @@ class ManagerProcess(ABC):
enabled = True
name = ""
shutting_down = False
restart_if_crash = False
@abstractmethod
def prepare(self) -> None:
@@ -167,13 +168,14 @@ class NativeProcess(ManagerProcess):
class PythonProcess(ManagerProcess):
def __init__(self, name, module, should_run, enabled=True, sigkill=False):
def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False):
self.name = name
self.module = module
self.should_run = should_run
self.enabled = enabled
self.sigkill = sigkill
self.launcher = launcher
self.restart_if_crash = restart_if_crash
def prepare(self) -> None:
if self.enabled:
@@ -252,6 +254,9 @@ def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None
running = []
for p in procs:
if p.enabled and p.name not in not_run and p.should_run(started, params, CP):
if p.restart_if_crash and p.proc is not None and not p.proc.is_alive():
cloudlog.error(f'Restarting {p.name} (exitcode {p.proc.exitcode})')
p.restart()
running.append(p)
else:
p.stop(block=False)

View File

@@ -126,7 +126,7 @@ procs = [
PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)),
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
PythonProcess("ui", "selfdrive.ui.ui", always_run),
PythonProcess("ui", "selfdrive.ui.ui", always_run, restart_if_crash=True),
PythonProcess("soundd", "selfdrive.ui.soundd", driverview),
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),

View File

@@ -17,7 +17,7 @@ from cereal.messaging import SubMaster
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
from openpilot.common.utils import atomic_write_in_dir
from openpilot.common.utils import atomic_write
from openpilot.system.version import get_build_metadata
from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S
@@ -218,7 +218,7 @@ def main() -> NoReturn:
if len(os.listdir(STATS_DIR)) < STATS_DIR_FILE_LIMIT:
if len(result) > 0:
stats_path = os.path.join(STATS_DIR, f"{boot_uid}_{idx}")
with atomic_write_in_dir(stats_path) as f:
with atomic_write(stats_path) as f:
f.write(result)
idx += 1
else:

View File

@@ -10,6 +10,7 @@ Quick start:
* set `BURN_IN=1` to get a burn-in heatmap version of the UI
* set `GRID=50` to show a 50-pixel alignment grid overlay
* set `MAGIC_DEBUG=1` to show every dropped frames (only on device)
* set `RECORD=1` to record the screen, output defaults to `output.mp4` but can be set with `RECORD_OUTPUT`
* set `SUNNYPILOT_UI=0` to run the stock UI instead of the sunnypilot UI
* https://www.raylib.com/cheatsheet/cheatsheet.html
* https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart

View File

@@ -7,11 +7,13 @@ import sys
import pyray as rl
import threading
import platform
import subprocess
from contextlib import contextmanager
from collections.abc import Callable
from collections import deque
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from typing import NamedTuple
from importlib.resources import as_file, files
from openpilot.common.swaglog import cloudlog
@@ -38,6 +40,8 @@ SCALE = float(os.getenv("SCALE", "1.0"))
GRID_SIZE = int(os.getenv("GRID", "0"))
PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0"))
PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output
RECORD = os.getenv("RECORD") == "1"
RECORD_OUTPUT = str(Path(os.getenv("RECORD_OUTPUT", "output")).with_suffix(".mp4"))
GL_VERSION = """
#version 300 es
@@ -200,10 +204,15 @@ class GuiApplication(GuiApplicationExt):
else:
self._scale = SCALE
# Scale, then ensure dimensions are even
self._scaled_width = int(self._width * self._scale)
self._scaled_height = int(self._height * self._scale)
self._scaled_width += self._scaled_width % 2
self._scaled_height += self._scaled_height % 2
self._render_texture: rl.RenderTexture | None = None
self._burn_in_shader: rl.Shader | None = None
self._ffmpeg_proc: subprocess.Popen | None = None
self._textures: dict[str, rl.Texture] = {}
self._target_fps: int = _DEFAULT_FPS
self._last_fps_log_time: float = time.monotonic()
@@ -264,12 +273,33 @@ class GuiApplication(GuiApplicationExt):
rl.set_config_flags(flags)
rl.init_window(self._scaled_width, self._scaled_height, title)
needs_render_texture = self._scale != 1.0 or BURN_IN_MODE
needs_render_texture = self._scale != 1.0 or BURN_IN_MODE or RECORD
if self._scale != 1.0:
rl.set_mouse_scale(1 / self._scale, 1 / self._scale)
if needs_render_texture:
self._render_texture = rl.load_render_texture(self._width, self._height)
rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
if RECORD:
ffmpeg_args = [
'ffmpeg',
'-v', 'warning', # Reduce ffmpeg log spam
'-stats', # Show encoding progress
'-f', 'rawvideo', # Input format
'-pix_fmt', 'rgba', # Input pixel format
'-s', f'{self._width}x{self._height}', # Input resolution
'-r', str(fps), # Input frame rate
'-i', 'pipe:0', # Input from stdin
'-vf', 'vflip,format=yuv420p', # Flip vertically and convert rgba to yuv420p
'-c:v', 'libx264', # Video codec
'-preset', 'ultrafast', # Encoding speed
'-y', # Overwrite existing file
'-f', 'mp4', # Output format
RECORD_OUTPUT, # Output file path
]
self._ffmpeg_proc = subprocess.Popen(ffmpeg_args, stdin=subprocess.PIPE)
rl.set_target_fps(fps)
self._target_fps = fps
@@ -314,6 +344,9 @@ class GuiApplication(GuiApplicationExt):
def set_modal_overlay(self, overlay, callback: Callable | None = None):
if self._modal_overlay.overlay is not None:
if hasattr(self._modal_overlay.overlay, 'hide_event'):
self._modal_overlay.overlay.hide_event()
if self._modal_overlay.callback is not None:
self._modal_overlay.callback(-1)
@@ -377,6 +410,16 @@ class GuiApplication(GuiApplicationExt):
rl.unload_image(image)
return texture
def close_ffmpeg(self):
if self._ffmpeg_proc is not None:
self._ffmpeg_proc.stdin.flush()
self._ffmpeg_proc.stdin.close()
try:
self._ffmpeg_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self._ffmpeg_proc.terminate()
self._ffmpeg_proc.wait()
def close(self):
if not rl.is_window_ready():
return
@@ -400,6 +443,8 @@ class GuiApplication(GuiApplicationExt):
if not PC:
self._mouse.stop()
self.close_ffmpeg()
rl.close_window()
@property
@@ -477,6 +522,15 @@ class GuiApplication(GuiApplicationExt):
self._draw_grid()
rl.end_drawing()
if RECORD:
image = rl.load_image_from_texture(self._render_texture.texture)
data_size = image.width * image.height * 4
data = bytes(rl.ffi.buffer(image.data, data_size))
self._ffmpeg_proc.stdin.write(data)
self._ffmpeg_proc.stdin.flush()
rl.unload_image(image)
self._monitor_fps()
self._frame += 1
@@ -514,6 +568,8 @@ class GuiApplication(GuiApplicationExt):
# Clear the overlay and execute the callback
original_modal = self._modal_overlay
self._modal_overlay = ModalOverlay()
if hasattr(original_modal.overlay, 'hide_event'):
original_modal.overlay.hide_event()
if original_modal.callback is not None:
original_modal.callback(result)
return True
@@ -602,6 +658,7 @@ class GuiApplication(GuiApplicationExt):
# Strict mode: terminate UI if FPS drops too much
if STRICT_MODE and fps < self._target_fps * FPS_CRITICAL_THRESHOLD:
cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.")
self.close_ffmpeg()
os._exit(1)
def _draw_touch_points(self):

View File

@@ -128,8 +128,12 @@ def init_egl() -> bool:
def create_egl_image(width: int, height: int, stride: int, fd: int, uv_offset: int) -> EGLImage | None:
assert _egl.initialized, "EGL not initialized"
# Duplicate fd since EGL needs it
dup_fd = os.dup(fd)
try:
# Duplicate fd since EGL needs it
dup_fd = os.dup(fd)
except OSError as e:
cloudlog.exception(f"Failed to duplicate frame fd when creating EGL image: {e}")
return None
# Create image attributes for EGL
img_attrs = [

View File

@@ -8,7 +8,7 @@ from openpilot.system.ui.lib.application import gui_app, MouseEvent
from openpilot.system.hardware import TICI
from collections import deque
MIN_VELOCITY = 2 # px/s, changes from auto scroll to steady state
MIN_VELOCITY = 10 # px/s, changes from auto scroll to steady state
MIN_VELOCITY_FOR_CLICKING = 2 * 60 # px/s, accepts clicks while auto scrolling below this velocity
MIN_DRAG_PIXELS = 12
AUTO_SCROLL_TC_SNAP = 0.025
@@ -67,16 +67,18 @@ class GuiScrollPanel2:
print()
return self.get_offset()
def _get_offset_bounds(self, bounds_size: float, content_size: float) -> tuple[float, float]:
"""Returns (max_offset, min_offset) for the given bounds and content size."""
return 0.0, min(0.0, bounds_size - content_size)
def _update_state(self, bounds_size: float, content_size: float) -> None:
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
if self._state == ScrollState.AUTO_SCROLL:
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
# simple exponential return if out of bounds
out_of_bounds = self.get_offset() > 0 or self.get_offset() < (bounds_size - content_size)
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
if out_of_bounds and self._handle_out_of_bounds:
if self.get_offset() < (bounds_size - content_size): # too far right
target = bounds_size - content_size
else: # too far left
target = 0.0
target = max_offset if self.get_offset() > max_offset else min_offset
dt = rl.get_frame_time() or 1e-6
factor = 1.0 - math.exp(-BOUNCE_RETURN_RATE * dt)
@@ -88,6 +90,7 @@ class GuiScrollPanel2:
# Steady once we are close enough to the target
if abs(dist) < 1 and abs(self._velocity) < MIN_VELOCITY:
self.set_offset(target)
self._velocity = 0.0
self._state = ScrollState.STEADY
elif abs(self._velocity) < MIN_VELOCITY:
@@ -102,7 +105,9 @@ class GuiScrollPanel2:
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
content_size: float) -> None:
out_of_bounds = self.get_offset() > 0 or self.get_offset() < (bounds_size - content_size)
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
# simple exponential return if out of bounds
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
if DEBUG:
print('Mouse event:', mouse_event)
@@ -201,8 +206,8 @@ class GuiScrollPanel2:
def _get_mouse_pos(self, mouse_event: MouseEvent) -> float:
return mouse_event.pos.x if self._horizontal else mouse_event.pos.y
def get_offset(self) -> int:
return round(self._offset.x if self._horizontal else self._offset.y)
def get_offset(self) -> float:
return self._offset.x if self._horizontal else self._offset.y
def set_offset(self, value: float) -> None:
if self._horizontal:

View File

@@ -244,7 +244,7 @@ class TermsPage(Widget):
pass
def _render(self, _):
scroll_offset = self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16)
scroll_offset = round(self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16))
if scroll_offset <= self._scrolled_down_offset:
# don't show back if not enabled

View File

@@ -0,0 +1,12 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
class NoElideButtonAction(ButtonActionSP):
def get_width_hint(self):
return super().get_width_hint() + 1

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