Compare commits

...

84 Commits

Author SHA1 Message Date
Jason Wen 67eb5bbe6f bump 2026-02-13 19:59:41 -05:00
Jason Wen 3d5c8c6acb Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into toyota-can-filter-aeb
# Conflicts:
#	opendbc_repo
2026-02-13 19:41:01 -05:00
Yasuhiro Ohno b62014eb12 SCC-V: Use p97 for predicted lateral accel (#1635)
* SCC-V: Use p97 for predicted lateral accel

* SCC-V: add param tests for p97 max_pred_lat_acc

* fix

* typo

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-13 17:03:03 -05:00
Jason Wen 037e8c18f8 sunnylink: update subscribed services states while running (#1694) 2026-02-13 16:22:51 -05:00
royjr 4bd60cd3cd [MICI] ui: rainbow path (#1630)
* allow rainbow road on mici

* initialization

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-13 11:41:30 -05:00
Jason Wen 3fe33cbe97 [TIZI/TICI] ui: fix toggle states and simplify blindspots/turn signals updates (#1692)
* [TIZI/TICI] ui: fix toggle states and simplify blindspots/turn signals updates

* more
2026-02-13 11:33:52 -05:00
Christopher Haucke b3878fb211 Pause Lateral Control with Blinker: Post-Blinker Delay (#1567)
* Delay lateral reengagement

* UI elements

* Add tests

* Update title and description

* Update params_metadata

* Didn't mean to pass this to int()

* Keep sentry happy

* Title and description update

* always 100 hz

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-12 23:33:38 -05:00
Nayan 33a26ba373 ui: better wake mode support (#1578)
* wake mode support

* use ui_state.params

* better

* cleanup

* fix

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-12 23:09:24 -05:00
Jason Wen 58af294ffd [TIZI/TICI] ui: Cruise panel (#1691)
* init cruise panel

* init descriptions

* damn descriptions

* init SLA sub layout

* reorder

* icbm it

* callback for `Custom ACC Speed Increments` toggle to update dependent UI elements dynamically.

* works

* more init

* more

* [TIZI/TICI] ui: individual button states support for MultipleButtonActionSP

* less

* convert

* init properly

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
2026-02-12 22:56:18 -05:00
Jason Wen bafbfe19b4 [TIZI/TICI] ui: individual button states support for MultipleButtonActionSP (#1690)
* [TIZI/TICI] ui: individual button states support for MultipleButtonActionSP

* no magic nums
2026-02-12 22:32:15 -05:00
Jason Wen dae95af1df [TIZI/TICI] ui: override set_parent_rect to dynamically update item height (#1689)
[TIZI/TICI] ui: override `set_parent_rect` method to update item height dynamically
2026-02-12 22:07:05 -05:00
Jason Wen 86d52ab969 Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1687)
* clips: allow mici UI (now default) (#37070)

fix: make big false by default and remove assertion

* comma four: new keyboard enter button (#37072)

* works

* enter dis

* clean up

* clean up

* no debug

* useless

* poadding

* tools: fix Python version comparison using normalized semantic version format (#37075)

* Back to tethering BigButton (#37082)

Back to tethering big button

* Stock LKAS: remove permanent alert (#37083)

rm perm

* BigButton: remove unused scrolling (#37085)

* BigButton: remove unused scrolling

* clean up

* BigButton: use UnifiedLabel (#37086)

* BigButton: remove unused scrolling

* start

* debug

* stash

* actually removing the hardcoded size for multioption fixes it

* back

* cursor does sub label

* clean  up

* more

* more

* try this for now

* nick suggest

* clean up

* more

* more

* Network menu improvements (#37077)

* try this

* that's handy

* todo, not sure what happens here

* set_text

* normalize

* scroll wifi

* clean up

* more

* better check

* UI: only show `onroad_fade.png` when engaged (#37051)

* only show when engaged

* retrigger CI

* fade animation 0.1

* nl

---------

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

* NavBar: fix no show animation (#37090)

* 1.5 not enough time on small screen

* ohhhh

* clean up

* translations: remove arabic (#37087)

* remove arabic

* more

* bump opendbc (#37091)

* long_mpc: use log.capnp source enum instead of list (#37093)

* Revert "long_mpc: use log.capnp source enum instead of list" (#37095)

Revert "long_mpc: use log.capnp source enum instead of list (#37093)"

This reverts commit 7e959c5a3e.

* mici setup: remove unused functions

* fix mici setup int enum

* clips: improve overlays for mici (#37088)

* adjust overlay sizes and wrap metadata text for mici

* comment

* adjust overlay rendering to dynamically calculate line height for wrapped metadata text

* render time first so we can use width in margin calculation

* update comment (to retry failed CI actually)

* increase metadata size on mici

* longitudinal mpc tuning: behind if main (#37099)

* longitudinal mpc tuning report: add sinusoidal oscillation scenario (#37100)

* long_mpc: use log.capnp source enum (#37096)

* Calibrate in tg (#36621)

* squash

* bump tg

* fix linmt

* Ready to merge

* cleaner

* match modeld

* more dead stuff

* long mpc: state name before subscript (#37101)

* clip: clean up imports (#37104)

* wtf is this

* don't break import timing

* they are the same

* clean up

* good catch

* rm common/mat.h

* Remove all the OpenCL (#37105)

* Remove all the OpenCL

* lil more

* bump msgq

* clip: use wrap_text helper (#37102)

* they are same

* no duplication!

* rm rstrip

* bump opendbc (#37108)

bump

* Delete less dialogs (#37111)

* try

* revert

* this is fine

* revert tg calib and opencl cleanup (#37113)

* Revert "Remove all the OpenCL (#37105)"

This reverts commit d5cbb89d84.

* Revert "rm common/mat.h"

This reverts commit 4ce701150a.

* Revert "Calibrate in tg (#36621)"

This reverts commit 593c3a0c8e.

* fix: route fetch metadata when first log isnt uploaded (#37114)

* fix: route fetch metadata when first log isnt uploaded

* default

* simple

---------

Co-authored-by: Trey Moen <treymoen@amazon.com>

* gitignore .context/

* ui diff replay: remove unused frame_data list for individual frame display (#37117)

Remove unused base64 encoding and simplify frame data handling in diff video report

* bridge: move ZMQ handling over (#37118)

* replace tabulate with simple helper (#37122)

* Better memory usage debugging (#37120)

* Revert "revert tg calib and opencl cleanup (#37113)" (#37115)

* Revert "revert tg calib and opencl cleanup (#37113)"

This reverts commit 51312afd3d.

* power draw is a lil higher

* just don't miss a cycle

* fix warp targets

* fix tinygrad dep

* CI: garbage collect tmp jenkins branches (#37125)

* Build vendored artifacts in CI (#37127)

* Build vendored artifacts in CI

* parallel

* deterministic

* fix up

* fix gitignores

* lil more

* lil more consistency

* remove get_mcu_type from pandad.py (#37132)

* Chunk big model files (#37134)

* file chunking

* try this

* more cleanup

* cleaner

* ui: delay nav bar animation (#37137)

* from da bounce

* try this

* you can get it to clean up wow

* modeld: simplify model run processing (#37138)

Hi! The point of this pr is to make the model run easier to read. On the latest tinygrad numpy().flatten() empirically does the same thing as the internal contiguous().realize().uop.base.buffer.numpy(). numpy() is also documented (docstrings), which can assist new contributors in learning what each potential execution does. Torq_boi or yassine, I know you want proof in the code base, so here it is. As of tinygrad commit 2f55005:

 in tinygrad_repo/tinygrad/tensor.py
Lines 316-318 (def _buffer): ensure the tenso is contiguous() and realized() before accessing the raw buffer.
Line 378 (def numpy): Wraps the buffer access and adds a reshape to match the tensor shape.
self._buffer() is what executes contiguous().realize() and returns the buffer object.
Calling numpy() on that buffer object returns a 1D array (defined in tinygrad/device.py:193 via np.frombuffer).
The reshape(self.shape) at the end of Tensor.numpy() then adds dimensions to that 1D array. The added .flatten() removes those dimensions, flattening it back to a 1D array. Effectively the same as what is currently done, but less complex.

* Revert "Chunk big model files (#37134)" (#37139)

This reverts commit a941e8f78f.

* Process replay: move refs to ci-artifacts (#37049)

* rm upload

* use ci-artifacts

* sanitize

* rm ref_commit

* add ci

* handle exept

* bootstrap

* always

* fix

* replay

* keep ref_commit fork compatibility

* remove upload-only

* apply comments

* safe diffs in master

* Revert "safe diffs in master"

This reverts commit 369fccac786a67799193e9152488813c6df20414.

* continue on master diff

* Update .github/workflows/tests.yaml

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

---------

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

* fix first-interaction action inputs for v3 (#37144)

v3 renamed inputs from kebab-case to snake_case (repo-token -> repo_token,
pr-message -> pr_message). The old names were silently ignored, causing
"Input required and not supplied: issue_message" errors.

* bump create-pull-request action to v8.1.0 (#37143)

The pinned SHA was v6.0.4, which is incompatible with actions/checkout@v6
and causes a "Duplicate header: Authorization" 400 error during git
remote operations. See peter-evans/create-pull-request#4272.

* bump numpy to 2.4.2 (#37145)

* show dependency tree in weekly uv lock job (#37146)

* Revert "DM: Ford GT model" (#37148)

Revert "DM: Ford GT model (#37013)"

This reverts commit 1459d3519d.

* remove dead multilang for mici (#37150)

* ui: remove dead side button (#37151)

* rm side button

* fix

* fix

* BigButton: fix alignment and style (#37153)

* correct from bottom alignment

* temp

* fix scale animation w/ btn_y

* home settings are always 64

* cleanup

* some clean up

* make 23 const

* rev

* more

* remove azure deps (#37084)

* remove azure deps

* fix lint

* restore scripts

* [bot] Update Python packages (#37147)

* Update Python packages

* fix

* bump panda

* revert tinygrad

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>

* remove pytest-repeat (#37156)

* BigButton: push up all content when pressed (#37157)

clean implementation

* ui.py: fix stride (#37159)

fix uii.py

* BigButton: don't round drawn content (#37158)

* unround icons

* unround rest

* Revert tgwarp again (#37161)

* Reapply "revert tg calib and opencl cleanup (#37113)" (#37115)

This reverts commit 667f3bb32f.

* revert msgq too

* msgq on master

* newline in updater error

* Remove vertical scroll bar

* Simple scroll indicator (#37162)

* scroll indicator

* 65%

* calibrate

* margin

* cleaner?

* manual clean up

* clean up

* full scroll bar

* look

* looks

* unlook

* no fade, looks good but "too much"

* clean up

* cmt

* Scroll panel: adapt to content size shrinking (#37163)

fix

* WifiManager: sort by known networks (#37164)

sort by known

* mpc tuning report: minor improvements (#37167)

---------

Co-authored-by: David <49467229+TheSecurityDev@users.noreply.github.com>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: ugtthis <142481257+ugtthis@users.noreply.github.com>
Co-authored-by: royjr <royjr96@gmail.com>
Co-authored-by: Jason Young <46612682+jyoung8607@users.noreply.github.com>
Co-authored-by: felsager <76905857+felsager@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Trey Moen <50057480+greatgitsby@users.noreply.github.com>
Co-authored-by: Trey Moen <treymoen@amazon.com>
Co-authored-by: Andi Radulescu <andi.radulescu@gmail.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: ZwX1616 <zwx1616@gmail.com>
Co-authored-by: commaci-public <60409688+commaci-public@users.noreply.github.com>
Co-authored-by: Vehicle Researcher <user@comma.ai>
2026-02-11 20:35:55 -05:00
Jason Wen 52fb0b8171 Merge branch 'upstream/openpilot/master' into sync-20260211
# Conflicts:
#	.github/workflows/auto_pr_review.yaml
#	.github/workflows/tests.yaml
#	opendbc_repo
#	panda
#	selfdrive/pandad/pandad.py
#	selfdrive/test/process_replay/test_processes.py
2026-02-11 20:16:02 -05:00
felsager 5b98ea04ad mpc tuning report: minor improvements (#37167) 2026-02-11 10:21:12 -08:00
Shane Smiskol b9344af9bb WifiManager: sort by known networks (#37164)
sort by known
2026-02-11 01:23:14 -08:00
Shane Smiskol 1e0f1a8abc Scroll panel: adapt to content size shrinking (#37163)
fix
2026-02-11 01:23:00 -08:00
Shane Smiskol 8ba36b76a0 Simple scroll indicator (#37162)
* scroll indicator

* 65%

* calibrate

* margin

* cleaner?

* manual clean up

* clean up

* full scroll bar

* look

* looks

* unlook

* no fade, looks good but "too much"

* clean up

* cmt
2026-02-11 01:15:02 -08:00
Shane Smiskol 3f382d6e69 Remove vertical scroll bar 2026-02-11 00:18:02 -08:00
Shane Smiskol 10edb90ac6 newline in updater error 2026-02-10 23:27:38 -08:00
Harald Schäfer 45099e7fcd Revert tgwarp again (#37161)
* Reapply "revert tg calib and opencl cleanup (#37113)" (#37115)

This reverts commit 667f3bb32f.

* revert msgq too

* msgq on master
2026-02-10 23:12:41 -08:00
Shane Smiskol 77f069cbe5 BigButton: don't round drawn content (#37158)
* unround icons

* unround rest
2026-02-10 21:57:34 -08:00
Shane Smiskol 1070dda3ee ui.py: fix stride (#37159)
fix uii.py
2026-02-10 21:56:45 -08:00
Jason Wen decd7d4c1c [TIZI/TICI] ui: exclude angleState from fade effects when rendering torque bar (#1686) 2026-02-11 00:35:03 -05:00
Shane Smiskol fcd5897650 BigButton: push up all content when pressed (#37157)
clean implementation
2026-02-10 21:29:14 -08:00
Jason Wen dc5dfe7f3b maneuversd: keep services happy (#1685) 2026-02-11 00:27:57 -05:00
Jason Wen f9c57ff285 Revert "CI: enable macos tests (#37005)"
This reverts commit c179a3ccb7.
2026-02-11 00:20:23 -05:00
Adeeb Shihadeh f1785c245a remove pytest-repeat (#37156) 2026-02-10 20:53:02 -08:00
commaci-public 6892b62761 [bot] Update Python packages (#37147)
* Update Python packages

* fix

* bump panda

* revert tinygrad

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-02-10 20:48:34 -08:00
Jason Wen 3d3a5de409 [TIZI/TICI] ui: dynamic ICBM status (#1684)
* [TIZI/TICI] ui: dynamic ICBM status

* wat

* make sure to init

* make sure it exists

* send it for all
2026-02-10 22:58:59 -05:00
Jason Wen bef93ecf65 [TIZI/TICI] ui: fix Developer UI orders (#1682) 2026-02-10 22:24:18 -05:00
Daniel Koepping a18ddf12eb remove azure deps (#37084)
* remove azure deps

* fix lint

* restore scripts
2026-02-10 17:51:09 -08:00
Shane Smiskol 46ae67b607 BigButton: fix alignment and style (#37153)
* correct from bottom alignment

* temp

* fix scale animation w/ btn_y

* home settings are always 64

* cleanup

* some clean up

* make 23 const

* rev

* more
2026-02-10 17:15:59 -08:00
Shane Smiskol 4d3aeaba6d ui: remove dead side button (#37151)
* rm side button

* fix

* fix
2026-02-10 15:04:36 -08:00
Shane Smiskol ba67e468ab remove dead multilang for mici (#37150) 2026-02-10 14:53:25 -08:00
ZwX1616 e946e9de0b Revert "DM: Ford GT model" (#37148)
Revert "DM: Ford GT model (#37013)"

This reverts commit 1459d3519d.
2026-02-10 13:56:07 -08:00
Adeeb Shihadeh 7d2563880a show dependency tree in weekly uv lock job (#37146) 2026-02-10 09:31:50 -08:00
Andi Radulescu 43edc51cb6 bump numpy to 2.4.2 (#37145) 2026-02-10 09:20:52 -08:00
Andi Radulescu 9476a8a7f6 bump create-pull-request action to v8.1.0 (#37143)
The pinned SHA was v6.0.4, which is incompatible with actions/checkout@v6
and causes a "Duplicate header: Authorization" 400 error during git
remote operations. See peter-evans/create-pull-request#4272.
2026-02-10 09:19:56 -08:00
Andi Radulescu 053441fbb3 fix first-interaction action inputs for v3 (#37144)
v3 renamed inputs from kebab-case to snake_case (repo-token -> repo_token,
pr-message -> pr_message). The old names were silently ignored, causing
"Input required and not supplied: issue_message" errors.
2026-02-10 09:19:37 -08:00
Daniel Koepping e35a1eca99 Process replay: move refs to ci-artifacts (#37049)
* rm upload

* use ci-artifacts

* sanitize

* rm ref_commit

* add ci

* handle exept

* bootstrap

* always

* fix

* replay

* keep ref_commit fork compatibility

* remove upload-only

* apply comments

* safe diffs in master

* Revert "safe diffs in master"

This reverts commit 369fccac786a67799193e9152488813c6df20414.

* continue on master diff

* Update .github/workflows/tests.yaml

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

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2026-02-09 21:37:20 -08:00
Harald Schäfer 3d11e8ef36 Revert "Chunk big model files (#37134)" (#37139)
This reverts commit a941e8f78f.
2026-02-09 20:58:22 -08:00
James Vecellio-Grant 73f720220b modeld: simplify model run processing (#37138)
Hi! The point of this pr is to make the model run easier to read. On the latest tinygrad numpy().flatten() empirically does the same thing as the internal contiguous().realize().uop.base.buffer.numpy(). numpy() is also documented (docstrings), which can assist new contributors in learning what each potential execution does. Torq_boi or yassine, I know you want proof in the code base, so here it is. As of tinygrad commit 2f55005:

 in tinygrad_repo/tinygrad/tensor.py
Lines 316-318 (def _buffer): ensure the tenso is contiguous() and realized() before accessing the raw buffer.
Line 378 (def numpy): Wraps the buffer access and adds a reshape to match the tensor shape.
self._buffer() is what executes contiguous().realize() and returns the buffer object.
Calling numpy() on that buffer object returns a 1D array (defined in tinygrad/device.py:193 via np.frombuffer).
The reshape(self.shape) at the end of Tensor.numpy() then adds dimensions to that 1D array. The added .flatten() removes those dimensions, flattening it back to a 1D array. Effectively the same as what is currently done, but less complex.
2026-02-09 20:24:25 -08:00
Shane Smiskol a1202eaf2a ui: delay nav bar animation (#37137)
* from da bounce

* try this

* you can get it to clean up wow
2026-02-09 17:16:36 -08:00
Harald Schäfer a941e8f78f Chunk big model files (#37134)
* file chunking

* try this

* more cleanup

* cleaner
2026-02-09 15:29:50 -08:00
Andi Radulescu 9aca13cf2d remove get_mcu_type from pandad.py (#37132) 2026-02-09 09:36:04 -08:00
Adeeb Shihadeh ac087d085e Build vendored artifacts in CI (#37127)
* Build vendored artifacts in CI

* parallel

* deterministic

* fix up

* fix gitignores

* lil more

* lil more consistency
2026-02-08 09:59:33 -08:00
Adeeb Shihadeh 46d65095af CI: garbage collect tmp jenkins branches (#37125) 2026-02-07 23:04:01 -08:00
Adeeb Shihadeh 667f3bb32f Revert "revert tg calib and opencl cleanup (#37113)" (#37115)
* Revert "revert tg calib and opencl cleanup (#37113)"

This reverts commit 51312afd3d.

* power draw is a lil higher

* just don't miss a cycle

* fix warp targets

* fix tinygrad dep
2026-02-07 21:36:44 -08:00
Adeeb Shihadeh c65cf18c75 Better memory usage debugging (#37120) 2026-02-07 21:00:56 -08:00
Adeeb Shihadeh 55a31d7657 replace tabulate with simple helper (#37122) 2026-02-07 18:27:16 -08:00
Adeeb Shihadeh ac17c35cfe bridge: move ZMQ handling over (#37118) 2026-02-07 15:18:00 -08:00
David bcb13a7229 ui diff replay: remove unused frame_data list for individual frame display (#37117)
Remove unused base64 encoding and simplify frame data handling in diff video report
2026-02-07 14:19:08 -08:00
Adeeb Shihadeh 0ce28803ec gitignore .context/ 2026-02-07 12:20:34 -08:00
Trey Moen db8cd9f411 fix: route fetch metadata when first log isnt uploaded (#37114)
* fix: route fetch metadata when first log isnt uploaded

* default

* simple

---------

Co-authored-by: Trey Moen <treymoen@amazon.com>
2026-02-07 11:52:28 -08:00
Harald Schäfer 51312afd3d revert tg calib and opencl cleanup (#37113)
* Revert "Remove all the OpenCL (#37105)"

This reverts commit d5cbb89d84.

* Revert "rm common/mat.h"

This reverts commit 4ce701150a.

* Revert "Calibrate in tg (#36621)"

This reverts commit 593c3a0c8e.
2026-02-07 09:10:29 -08:00
Shane Smiskol efc23644c7 Delete less dialogs (#37111)
* try

* revert

* this is fine
2026-02-06 22:59:05 -08:00
Shane Smiskol e531f844f6 bump opendbc (#37108)
bump
2026-02-06 20:15:12 -08:00
Shane Smiskol 46f74753cd clip: use wrap_text helper (#37102)
* they are same

* no duplication!

* rm rstrip
2026-02-06 17:11:17 -08:00
Adeeb Shihadeh d5cbb89d84 Remove all the OpenCL (#37105)
* Remove all the OpenCL

* lil more

* bump msgq
2026-02-06 16:36:47 -08:00
Adeeb Shihadeh 4ce701150a rm common/mat.h 2026-02-06 16:06:16 -08:00
Shane Smiskol 96fded0399 clip: clean up imports (#37104)
* wtf is this

* don't break import timing

* they are the same

* clean up

* good catch
2026-02-06 15:13:08 -08:00
felsager 12597856da long mpc: state name before subscript (#37101) 2026-02-06 14:26:20 -08:00
Harald Schäfer 593c3a0c8e Calibrate in tg (#36621)
* squash

* bump tg

* fix linmt

* Ready to merge

* cleaner

* match modeld

* more dead stuff
2026-02-06 14:13:46 -08:00
felsager 187d3a079c long_mpc: use log.capnp source enum (#37096) 2026-02-06 13:36:51 -08:00
felsager 9755520b87 longitudinal mpc tuning report: add sinusoidal oscillation scenario (#37100) 2026-02-06 11:30:49 -08:00
felsager 7099bd18e3 longitudinal mpc tuning: behind if main (#37099) 2026-02-06 10:35:54 -08:00
David cb670c2c3d clips: improve overlays for mici (#37088)
* adjust overlay sizes and wrap metadata text for mici

* comment

* adjust overlay rendering to dynamically calculate line height for wrapped metadata text

* render time first so we can use width in margin calculation

* update comment (to retry failed CI actually)

* increase metadata size on mici
2026-02-06 09:58:30 -08:00
Shane Smiskol f5b84e74f4 fix mici setup int enum 2026-02-05 23:42:16 -08:00
Shane Smiskol 2b8e887e44 mici setup: remove unused functions 2026-02-05 23:39:22 -08:00
Harald Schäfer 64f74dad27 Revert "long_mpc: use log.capnp source enum instead of list" (#37095)
Revert "long_mpc: use log.capnp source enum instead of list (#37093)"

This reverts commit 7e959c5a3e.
2026-02-05 16:23:28 -08:00
felsager 7e959c5a3e long_mpc: use log.capnp source enum instead of list (#37093) 2026-02-05 15:55:03 -08:00
Jason Young 28795d3065 bump opendbc (#37091) 2026-02-05 16:30:03 -05:00
royjr 8aed5a1a89 translations: remove arabic (#37087)
* remove arabic

* more
2026-02-05 09:28:01 -08:00
Shane Smiskol a5c973be89 NavBar: fix no show animation (#37090)
* 1.5 not enough time on small screen

* ohhhh

* clean up
2026-02-04 23:52:59 -08:00
ugtthis 39dcc88330 UI: only show onroad_fade.png when engaged (#37051)
* only show when engaged

* retrigger CI

* fade animation 0.1

* nl

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2026-02-04 23:03:01 -08:00
Shane Smiskol fc6afd5ab8 Network menu improvements (#37077)
* try this

* that's handy

* todo, not sure what happens here

* set_text

* normalize

* scroll wifi

* clean up

* more

* better check
2026-02-04 22:58:16 -08:00
Shane Smiskol 177a1a3c8b BigButton: use UnifiedLabel (#37086)
* BigButton: remove unused scrolling

* start

* debug

* stash

* actually removing the hardcoded size for multioption fixes it

* back

* cursor does sub label

* clean  up

* more

* more

* try this for now

* nick suggest

* clean up

* more

* more
2026-02-04 22:25:28 -08:00
Jason Wen c9016adb3a Toyota: do not disable radar when smartDSU or CAN Filter detected 2026-02-05 00:17:11 -05:00
Shane Smiskol 1979a6d8e8 BigButton: remove unused scrolling (#37085)
* BigButton: remove unused scrolling

* clean up
2026-02-04 19:30:20 -08:00
Shane Smiskol 944c23f369 Stock LKAS: remove permanent alert (#37083)
rm perm
2026-02-04 17:06:11 -08:00
Shane Smiskol 2230933d4b Back to tethering BigButton (#37082)
Back to tethering big button
2026-02-04 16:38:24 -08:00
Jason Wen 3ea474dd58 tools: fix Python version comparison using normalized semantic version format (#37075) 2026-02-04 16:31:44 -08:00
Shane Smiskol 8879d481e5 comma four: new keyboard enter button (#37072)
* works

* enter dis

* clean up

* clean up

* no debug

* useless

* poadding
2026-02-04 16:04:49 -08:00
David 5e35feb3ab clips: allow mici UI (now default) (#37070)
fix: make big false by default and remove assertion
2026-02-04 11:25:09 -08:00
111 changed files with 2342 additions and 2775 deletions
+38 -1
View File
@@ -5,7 +5,44 @@ on:
types: [created, edited]
jobs:
# TODO: gc old branches in a separate job in this workflow
cleanup-branches:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Delete stale Jenkins branches
uses: actions/github-script@v8
with:
script: |
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
const prefixes = ['tmp-jenkins', '__jenkins'];
for await (const response of github.paginate.iterator(github.rest.repos.listBranches, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
})) {
for (const branch of response.data) {
if (!prefixes.some(p => branch.name.startsWith(p))) continue;
const { data: commit } = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: branch.commit.sha,
});
const commitDate = new Date(commit.commit.committer.date).getTime();
if (commitDate < cutoff) {
console.log(`Deleting branch: ${branch.name} (last commit: ${commit.commit.committer.date})`);
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branch.name}`,
});
}
}
}
scan-comments:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request }}
+14 -3
View File
@@ -21,7 +21,7 @@ jobs:
run: |
${{ env.RUN }} "python3 selfdrive/ui/update_translations.py --vanish"
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
commit-message: "Update translations"
@@ -48,6 +48,12 @@ jobs:
python3 -m ensurepip --upgrade
pip3 install uv
uv lock --upgrade
- name: uv pip tree
id: pip_tree
run: |
echo 'PIP_TREE<<EOF' >> $GITHUB_OUTPUT
uv pip tree >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: bump submodules
run: |
git config --global --add safe.directory '*'
@@ -61,7 +67,7 @@ jobs:
python selfdrive/car/docs.py
git add docs/CARS.md
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }}
@@ -70,5 +76,10 @@ jobs:
branch: auto-package-updates
base: master
delete-branch: true
body: 'Automatic PR from repo-maintenance -> package_updates'
body: |
Automatic PR from repo-maintenance -> package_updates
```
${{ steps.pip_tree.outputs.PIP_TREE }}
```
labels: bot
+22 -6
View File
@@ -20,8 +20,6 @@ concurrency:
env:
PYTHONWARNINGS: error
BASE_IMAGE: sunnypilot-base
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: release/ci/docker_build_sp.sh base
@@ -107,6 +105,7 @@ jobs:
build_mac:
name: build macOS
if: false # tmp disable due to brew install not working
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
steps:
- uses: actions/checkout@v6
@@ -216,12 +215,13 @@ jobs:
uses: actions/cache@v5
with:
path: .ci_cache/comma_download_cache
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }}
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/test_processes.py') }}
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Run replay
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
continue-on-error: ${{ github.ref == 'refs/heads/master' }}
run: |
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache"
@@ -235,10 +235,26 @@ jobs:
with:
name: process_replay_diff.txt
path: selfdrive/test/process_replay/diff.txt
- name: Upload reference logs
if: false # TODO: move this to github instead of azure
- name: Checkout ci-artifacts
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/ci-artifacts
- name: Push refs
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
working-directory: ${{ github.workspace }}/ci-artifacts
run: |
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
git checkout --orphan process-replay
git rm -rf .
git config user.name "GitHub Actions Bot"
git config user.email "<>"
cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst .
echo "${{ github.sha }}" > ref_commit
git add .
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}"
git push origin process-replay --force
- name: Run regen
if: false
timeout-minutes: 4
+51
View File
@@ -0,0 +1,51 @@
name: vendor third_party
on:
workflow_dispatch:
jobs:
build:
if: github.ref != 'refs/heads/master'
strategy:
matrix:
os: [ubuntu-24.04, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Build
run: third_party/build.sh
- name: Package artifacts
run: |
git add -A third_party/
git diff --cached --name-only -- third_party/ | tar -cf /tmp/third_party_build.tar -T -
- uses: actions/upload-artifact@v4
with:
name: third-party-${{ runner.os }}
path: /tmp/third_party_build.tar
commit:
needs: build
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
path: /tmp/artifacts
- name: Commit vendored libraries
run: |
for f in /tmp/artifacts/*/third_party_build.tar; do
tar xf "$f"
done
git add third_party/
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "third_party: rebuild vendor libraries"
git push
+1
View File
@@ -100,6 +100,7 @@ Pipfile
.ionide
.claude/
.context/
PLAN.md
TASK.md
+1 -1
View File
@@ -13,7 +13,7 @@ cereal = env.Library('cereal', [f'gen/cpp/{s}.c++' for s in schema_files])
# Build messaging
services_h = env.Command(['services.h'], ['services.py'], 'python3 ' + cereal_dir.path + '/services.py > $TARGET')
env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc'], LIBS=[msgq, common, 'pthread'])
env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread'])
socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc'])
+7 -2
View File
@@ -1478,6 +1478,11 @@ struct ProcLog {
cmdline @15 :List(Text);
exe @16 :Text;
# from /proc/<pid>/smaps_rollup (proportional/private memory)
memPss @17 :UInt64; # Pss — shared pages split by mapper count
memPssAnon @18 :UInt64; # Pss_Anon — private anonymous (heap, stack)
memPssShmem @19 :UInt64; # Pss_Shmem — proportional MSGQ/tmpfs share
}
struct CPUTimes {
@@ -2227,9 +2232,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
isActiveMode @16 :Bool;
isRHD @4 :Bool;
uncertainCount @19 :UInt32;
phoneProbOffset @20 :Float32;
phoneProbValidCount @21 :UInt32;
phoneProbOffsetDEPRECATED @20 :Float32;
phoneProbValidCountDEPRECATED @21 :UInt32;
isPreviewDEPRECATED @15 :Bool;
rhdCheckedDEPRECATED @5 :Bool;
eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED);
+6 -6
View File
@@ -25,14 +25,14 @@ void msgq_to_zmq(const std::vector<std::string> &endpoints, const std::string &i
}
void zmq_to_msgq(const std::vector<std::string> &endpoints, const std::string &ip) {
auto poller = std::make_unique<ZMQPoller>();
auto pub_context = std::make_unique<MSGQContext>();
auto sub_context = std::make_unique<ZMQContext>();
std::map<SubSocket *, PubSocket *> sub2pub;
auto poller = std::make_unique<BridgeZmqPoller>();
auto pub_context = std::make_unique<Context>();
auto sub_context = std::make_unique<BridgeZmqContext>();
std::map<BridgeZmqSubSocket *, PubSocket *> sub2pub;
for (auto endpoint : endpoints) {
auto pub_sock = new MSGQPubSocket();
auto sub_sock = new ZMQSubSocket();
auto pub_sock = new PubSocket();
auto sub_sock = new BridgeZmqSubSocket();
size_t queue_size = services.at(endpoint).queue_size;
pub_sock->connect(pub_context.get(), endpoint, true, queue_size);
sub_sock->connect(sub_context.get(), endpoint, ip, false);
+170
View File
@@ -0,0 +1,170 @@
#include "cereal/messaging/bridge_zmq.h"
#include <cassert>
#include <cstring>
#include <unistd.h>
static size_t fnv1a_hash(const std::string &str) {
const size_t fnv_prime = 0x100000001b3;
size_t hash_value = 0xcbf29ce484222325;
for (char c : str) {
hash_value ^= (unsigned char)c;
hash_value *= fnv_prime;
}
return hash_value;
}
// FIXME: This is a hack to get the port number from the socket name, might have collisions.
static int get_port(std::string endpoint) {
size_t hash_value = fnv1a_hash(endpoint);
int start_port = 8023;
int max_port = 65535;
return start_port + (hash_value % (max_port - start_port));
}
BridgeZmqContext::BridgeZmqContext() {
context = zmq_ctx_new();
}
BridgeZmqContext::~BridgeZmqContext() {
if (context != nullptr) {
zmq_ctx_term(context);
}
}
void BridgeZmqMessage::init(size_t sz) {
size = sz;
data = new char[size];
}
void BridgeZmqMessage::init(char *d, size_t sz) {
size = sz;
data = new char[size];
memcpy(data, d, size);
}
void BridgeZmqMessage::close() {
if (size > 0) {
delete[] data;
}
data = nullptr;
size = 0;
}
BridgeZmqMessage::~BridgeZmqMessage() {
close();
}
int BridgeZmqSubSocket::connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate, bool check_endpoint) {
sock = zmq_socket(context->getRawContext(), ZMQ_SUB);
if (sock == nullptr) {
return -1;
}
zmq_setsockopt(sock, ZMQ_SUBSCRIBE, "", 0);
if (conflate) {
int arg = 1;
zmq_setsockopt(sock, ZMQ_CONFLATE, &arg, sizeof(int));
}
int reconnect_ivl = 500;
zmq_setsockopt(sock, ZMQ_RECONNECT_IVL_MAX, &reconnect_ivl, sizeof(reconnect_ivl));
full_endpoint = "tcp://" + address + ":";
if (check_endpoint) {
full_endpoint += std::to_string(get_port(endpoint));
} else {
full_endpoint += endpoint;
}
return zmq_connect(sock, full_endpoint.c_str());
}
void BridgeZmqSubSocket::setTimeout(int timeout) {
zmq_setsockopt(sock, ZMQ_RCVTIMEO, &timeout, sizeof(int));
}
Message *BridgeZmqSubSocket::receive(bool non_blocking) {
zmq_msg_t msg;
assert(zmq_msg_init(&msg) == 0);
int flags = non_blocking ? ZMQ_DONTWAIT : 0;
int rc = zmq_msg_recv(&msg, sock, flags);
Message *ret = nullptr;
if (rc >= 0) {
ret = new BridgeZmqMessage;
ret->init((char *)zmq_msg_data(&msg), zmq_msg_size(&msg));
}
zmq_msg_close(&msg);
return ret;
}
BridgeZmqSubSocket::~BridgeZmqSubSocket() {
if (sock != nullptr) {
zmq_close(sock);
}
}
int BridgeZmqPubSocket::connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint) {
sock = zmq_socket(context->getRawContext(), ZMQ_PUB);
if (sock == nullptr) {
return -1;
}
full_endpoint = "tcp://*:";
if (check_endpoint) {
full_endpoint += std::to_string(get_port(endpoint));
} else {
full_endpoint += endpoint;
}
// ZMQ pub sockets cannot be shared between processes, so we need to ensure pid stays the same.
pid = getpid();
return zmq_bind(sock, full_endpoint.c_str());
}
int BridgeZmqPubSocket::sendMessage(Message *message) {
assert(pid == getpid());
return zmq_send(sock, message->getData(), message->getSize(), ZMQ_DONTWAIT);
}
int BridgeZmqPubSocket::send(char *data, size_t size) {
assert(pid == getpid());
return zmq_send(sock, data, size, ZMQ_DONTWAIT);
}
BridgeZmqPubSocket::~BridgeZmqPubSocket() {
if (sock != nullptr) {
zmq_close(sock);
}
}
void BridgeZmqPoller::registerSocket(BridgeZmqSubSocket *socket) {
assert(num_polls + 1 < (sizeof(polls) / sizeof(polls[0])));
polls[num_polls].socket = socket->getRawSocket();
polls[num_polls].events = ZMQ_POLLIN;
sockets.push_back(socket);
num_polls++;
}
std::vector<BridgeZmqSubSocket *> BridgeZmqPoller::poll(int timeout) {
std::vector<BridgeZmqSubSocket *> ret;
int rc = zmq_poll(polls, num_polls, timeout);
if (rc < 0) {
return ret;
}
for (size_t i = 0; i < num_polls; i++) {
if (polls[i].revents) {
ret.push_back(sockets[i]);
}
}
return ret;
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include <zmq.h>
#include "msgq/ipc.h"
class BridgeZmqContext {
public:
BridgeZmqContext();
void *getRawContext() { return context; }
~BridgeZmqContext();
private:
void *context = nullptr;
};
class BridgeZmqMessage : public Message {
public:
void init(size_t size);
void init(char *data, size_t size);
void close();
size_t getSize() { return size; }
char *getData() { return data; }
~BridgeZmqMessage();
private:
char *data = nullptr;
size_t size = 0;
};
class BridgeZmqSubSocket {
public:
int connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate = false, bool check_endpoint = true);
void setTimeout(int timeout);
Message *receive(bool non_blocking = false);
void *getRawSocket() { return sock; }
~BridgeZmqSubSocket();
private:
void *sock = nullptr;
std::string full_endpoint;
};
class BridgeZmqPubSocket {
public:
int connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint = true);
int sendMessage(Message *message);
int send(char *data, size_t size);
void *getRawSocket() { return sock; }
~BridgeZmqPubSocket();
private:
void *sock = nullptr;
std::string full_endpoint;
int pid = -1;
};
class BridgeZmqPoller {
public:
void registerSocket(BridgeZmqSubSocket *socket);
std::vector<BridgeZmqSubSocket *> poll(int timeout);
private:
static constexpr size_t MAX_BRIDGE_ZMQ_POLLERS = 128;
std::vector<BridgeZmqSubSocket *> sockets;
zmq_pollitem_t polls[MAX_BRIDGE_ZMQ_POLLERS] = {};
size_t num_polls = 0;
};
+6 -6
View File
@@ -22,14 +22,14 @@ static std::string recv_zmq_msg(void *sock) {
}
void MsgqToZmq::run(const std::vector<std::string> &endpoints, const std::string &ip) {
zmq_context = std::make_unique<ZMQContext>();
msgq_context = std::make_unique<MSGQContext>();
zmq_context = std::make_unique<BridgeZmqContext>();
msgq_context = std::make_unique<Context>();
// Create ZMQPubSockets for each endpoint
for (const auto &endpoint : endpoints) {
auto &socket_pair = socket_pairs.emplace_back();
socket_pair.endpoint = endpoint;
socket_pair.pub_sock = std::make_unique<ZMQPubSocket>();
socket_pair.pub_sock = std::make_unique<BridgeZmqPubSocket>();
int ret = socket_pair.pub_sock->connect(zmq_context.get(), endpoint);
if (ret != 0) {
printf("Failed to create ZMQ publisher for [%s]: %s\n", endpoint.c_str(), zmq_strerror(zmq_errno()));
@@ -49,7 +49,7 @@ void MsgqToZmq::run(const std::vector<std::string> &endpoints, const std::string
for (auto sub_sock : msgq_poller->poll(100)) {
// Process messages for each socket
ZMQPubSocket *pub_sock = sub2pub.at(sub_sock);
BridgeZmqPubSocket *pub_sock = sub2pub.at(sub_sock);
for (int i = 0; i < MAX_MESSAGES_PER_SOCKET; ++i) {
auto msg = std::unique_ptr<Message>(sub_sock->receive(true));
if (!msg) break;
@@ -72,7 +72,7 @@ void MsgqToZmq::zmqMonitorThread() {
// Set up ZMQ monitor for each pub socket
for (int i = 0; i < socket_pairs.size(); ++i) {
std::string addr = "inproc://op-bridge-monitor-" + std::to_string(i);
zmq_socket_monitor(socket_pairs[i].pub_sock->sock, addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
void *monitor_socket = zmq_socket(zmq_context->getRawContext(), ZMQ_PAIR);
zmq_connect(monitor_socket, addr.c_str());
@@ -130,7 +130,7 @@ void MsgqToZmq::zmqMonitorThread() {
// Clean up monitor sockets
for (int i = 0; i < pollitems.size(); ++i) {
zmq_socket_monitor(socket_pairs[i].pub_sock->sock, nullptr, 0);
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), nullptr, 0);
zmq_close(pollitems[i].socket);
}
cv.notify_one();
+5 -6
View File
@@ -7,9 +7,8 @@
#include <string>
#include <vector>
#define private public
#include "msgq/impl_msgq.h"
#include "msgq/impl_zmq.h"
#include "cereal/messaging/bridge_zmq.h"
class MsgqToZmq {
public:
@@ -22,16 +21,16 @@ protected:
struct SocketPair {
std::string endpoint;
std::unique_ptr<ZMQPubSocket> pub_sock;
std::unique_ptr<BridgeZmqPubSocket> pub_sock;
std::unique_ptr<MSGQSubSocket> sub_sock;
int connected_clients = 0;
};
std::unique_ptr<MSGQContext> msgq_context;
std::unique_ptr<ZMQContext> zmq_context;
std::unique_ptr<Context> msgq_context;
std::unique_ptr<BridgeZmqContext> zmq_context;
std::mutex mutex;
std::condition_variable cv;
std::unique_ptr<MSGQPoller> msgq_poller;
std::map<SubSocket *, ZMQPubSocket *> sub2pub;
std::map<SubSocket *, BridgeZmqPubSocket *> sub2pub;
std::vector<SocketPair> socket_pairs;
};
+1
View File
@@ -137,6 +137,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
{"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h
{"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}},
{"Brightness", {PERSISTENT | BACKUP, INT, "0"}},
+86
View File
@@ -167,6 +167,92 @@ def managed_proc(cmd: list[str], env: dict[str, str]):
proc.kill()
def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None):
rows = [list(row) for row in tabular_data]
def fmt(val):
if isinstance(val, str):
return val
if isinstance(val, (bool, int)):
return str(val)
try:
return format(val, floatfmt)
except (TypeError, ValueError):
return str(val)
formatted = [[fmt(c) for c in row] for row in rows]
hdrs = [str(h) for h in headers] if headers else None
ncols = max((len(r) for r in formatted), default=0)
if hdrs:
ncols = max(ncols, len(hdrs))
if ncols == 0:
return ""
for r in formatted:
r.extend([""] * (ncols - len(r)))
if hdrs:
hdrs.extend([""] * (ncols - len(hdrs)))
widths = [0] * ncols
if hdrs:
for i in range(ncols):
widths[i] = len(hdrs[i])
for row in formatted:
for i in range(ncols):
widths[i] = max(widths[i], max(len(ln) for ln in row[i].split('\n')))
def _align(s, w):
if stralign == "center":
return s.center(w)
return s.ljust(w)
if tablefmt == "html":
parts = ["<table>"]
if hdrs:
parts.append("<thead>")
parts.append("<tr>" + "".join(f"<th>{h}</th>" for h in hdrs) + "</tr>")
parts.append("</thead>")
parts.append("<tbody>")
for row in formatted:
parts.append("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>")
parts.append("</tbody>")
parts.append("</table>")
return "\n".join(parts)
if tablefmt == "simple_grid":
def _sep(left, mid, right):
return left + mid.join("\u2500" * (w + 2) for w in widths) + right
top, mid_sep, bot = _sep("\u250c", "\u252c", "\u2510"), _sep("\u251c", "\u253c", "\u2524"), _sep("\u2514", "\u2534", "\u2518")
def _fmt_row(cells):
split = [c.split('\n') for c in cells]
nlines = max(len(s) for s in split)
for s in split:
s.extend([""] * (nlines - len(s)))
return ["\u2502" + "\u2502".join(f" {_align(split[i][li], widths[i])} " for i in range(ncols)) + "\u2502" for li in range(nlines)]
lines = [top]
if hdrs:
lines.extend(_fmt_row(hdrs))
lines.append(mid_sep)
for ri, row in enumerate(formatted):
lines.extend(_fmt_row(row))
lines.append(mid_sep if ri < len(formatted) - 1 else bot)
return "\n".join(lines)
# simple
gap = " "
lines = []
if hdrs:
lines.append(gap.join(h.ljust(w) for h, w in zip(hdrs, widths, strict=True)))
lines.append(gap.join("-" * w for w in widths))
for row in formatted:
lines.append(gap.join(_align(row[i], widths[i]) for i in range(ncols)))
return "\n".join(lines)
def retry(attempts=3, delay=1.0, ignore_failure=False):
def decorator(func):
@functools.wraps(func)
+1 -1
Submodule panda updated: ed8a6f9ec2...a95e060e85
-4
View File
@@ -89,7 +89,6 @@ testing = [
"pytest-timeout",
"pytest-asyncio",
"pytest-mock",
"pytest-repeat",
"ruff",
"codespell",
"pre-commit-hooks",
@@ -97,15 +96,12 @@ testing = [
dev = [
"av",
"azure-identity",
"azure-storage-blob",
"dictdiffer",
"matplotlib",
"opencv-python-headless",
"parameterized >=0.8, <0.9",
"pyautogui",
"pywinctl",
"tabulate",
]
tools = [
+1 -1
View File
@@ -11,7 +11,7 @@ LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
GLYPH_PADDING = 6
EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥"
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
def _languages():
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9df44871e9f5fa910622b0b92205b92a54d137dbdc3827b92e8622d85ff2e08e
size 5189
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:013b368b38b17d9b2ef6aaf0f498f672deed95888084b7287f42bdfba617cbb6
size 10142
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8fd563eec78d5ce4a8204c2f596789e1090cb3e26a35b4ffeacee4ab61968538
size 8303
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0be8d5eddcd9f87acbf1daccf446be6218522120f64aee1ee0a3c0b31560f076
size 15761
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af8d5ecb6468442361462aa838a2d234b1256b8139418be8ef2962e4350cfbef
size 2176
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43b64365a42d7bf772d567b8867a6ced4ec0175bb88b6acaa3a5345f19ca696e
size 1268
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3dd956d5ccfce01a01bea74ef59c9e73dfca406a5ff9ac62417203afa6027fba
size 5620
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1dd1c2308872729d58adab390030ae9c987dc7908f0c39391651ea2b6cb620c5
size 2445
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88e6c50358f627fc714c1e9883143aeed00baabeab16132e16001aa1051e5eb8
size 1272
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fce940a3cbd2e9530e8efdde90794013a272919b2f3ea482bc06535c795640e7
size 2176
@@ -22,7 +22,8 @@ LONG_MPC_DIR = os.path.dirname(os.path.abspath(__file__))
EXPORT_DIR = os.path.join(LONG_MPC_DIR, "c_generated_code")
JSON_FILE = os.path.join(LONG_MPC_DIR, "acados_ocp_long.json")
SOURCES = ['lead0', 'lead1', 'cruise', 'e2e']
LongitudinalPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1, LongitudinalPlanSource.cruise)
X_DIM = 3
U_DIM = 1
@@ -107,10 +108,10 @@ def gen_long_model():
a_min = SX.sym('a_min')
a_max = SX.sym('a_max')
x_obstacle = SX.sym('x_obstacle')
prev_a = SX.sym('prev_a')
a_prev = SX.sym('a_prev')
lead_t_follow = SX.sym('lead_t_follow')
lead_danger_factor = SX.sym('lead_danger_factor')
model.p = vertcat(a_min, a_max, x_obstacle, prev_a, lead_t_follow, lead_danger_factor)
model.p = vertcat(a_min, a_max, x_obstacle, a_prev, lead_t_follow, lead_danger_factor)
# dynamics model
f_expl = vertcat(v_ego, a_ego, j_ego)
@@ -142,7 +143,7 @@ def gen_long_ocp():
a_min, a_max = ocp.model.p[0], ocp.model.p[1]
x_obstacle = ocp.model.p[2]
prev_a = ocp.model.p[3]
a_prev = ocp.model.p[3]
lead_t_follow = ocp.model.p[4]
lead_danger_factor = ocp.model.p[5]
@@ -159,7 +160,7 @@ def gen_long_ocp():
x_ego,
v_ego,
a_ego,
a_ego - prev_a,
a_ego - a_prev,
j_ego]
ocp.model.cost_y_expr = vertcat(*costs)
ocp.model.cost_y_expr_e = vertcat(*costs[:-1])
@@ -217,7 +218,7 @@ class LongitudinalMpc:
self.dt = dt
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.reset()
self.source = SOURCES[2]
self.source = LongitudinalPlanSource.cruise
def reset(self):
self.solver.reset()
@@ -227,7 +228,7 @@ class LongitudinalMpc:
self.v_solution = np.zeros(N+1)
self.a_solution = np.zeros(N+1)
self.j_solution = np.zeros(N)
self.prev_a = np.array(self.a_solution)
self.a_prev = np.array(self.a_solution)
self.yref = np.zeros((N+1, COST_DIM))
for i in range(N):
@@ -335,7 +336,7 @@ class LongitudinalMpc:
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
self.source = SOURCES[np.argmin(x_obstacles[0])]
self.source = MPC_SOURCES[np.argmin(x_obstacles[0])]
self.yref[:,:] = 0.0
for i in range(N):
@@ -345,7 +346,7 @@ class LongitudinalMpc:
self.params[:,0] = ACCEL_MIN
self.params[:,1] = ACCEL_MAX
self.params[:,2] = np.min(x_obstacles, axis=1)
self.params[:,3] = np.copy(self.prev_a)
self.params[:,3] = np.copy(self.a_prev)
self.params[:,4] = t_follow
self.params[:,5] = LEAD_DANGER_FACTOR
@@ -377,7 +378,7 @@ class LongitudinalMpc:
self.a_solution = self.x_sol[:,2]
self.j_solution = self.u_sol[:,0]
self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
self.a_prev = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
t = time.monotonic()
if self.solution_status != 0:
@@ -9,7 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
@@ -164,7 +164,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
if output_a_target < output_a_target_mpc:
self.mpc.source = SOURCES[3]
self.mpc.source = LongitudinalPlanSource.e2e
else:
output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
import argparse
import os
from collections import defaultdict
import numpy as np
from tabulate import tabulate
from openpilot.tools.lib.logreader import LogReader
DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
MB = 1024 * 1024
TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center")
def _get_procs():
from openpilot.selfdrive.test.test_onroad import PROCS
return PROCS
def is_openpilot_proc(name):
if any(p in name for p in _get_procs()):
return True
# catch openpilot processes not in PROCS (athenad, manager, etc.)
return 'openpilot' in name or name.startswith(('selfdrive.', 'system.'))
def get_proc_name(proc):
if len(proc.cmdline) > 0:
return list(proc.cmdline)[0]
return proc.name
def pct(val_mb, total_mb):
return val_mb / total_mb * 100 if total_mb else 0
def has_pss(proc_logs):
"""Check if logs contain PSS data (new field, not in old logs)."""
try:
for proc in proc_logs[-1].procLog.procs:
if proc.memPss > 0:
return True
except AttributeError:
pass
return False
def print_summary(proc_logs, device_states):
mem = proc_logs[-1].procLog.mem
total = mem.total / MB
used = (mem.total - mem.available) / MB
cached = mem.cached / MB
shared = mem.shared / MB
buffers = mem.buffers / MB
lines = [
f" Total: {total:.0f} MB",
f" Used (total-avail): {used:.0f} MB ({pct(used, total):.0f}%)",
f" Cached: {cached:.0f} MB ({pct(cached, total):.0f}%) Buffers: {buffers:.0f} MB ({pct(buffers, total):.0f}%)",
f" Shared/MSGQ: {shared:.0f} MB ({pct(shared, total):.0f}%)",
]
if device_states:
mem_pcts = [m.deviceState.memoryUsagePercent for m in device_states]
lines.append(f" deviceState memory: {np.min(mem_pcts)}-{np.max(mem_pcts)}% (avg {np.mean(mem_pcts):.0f}%)")
print("\n-- Memory Summary --")
print("\n".join(lines))
return total
def collect_per_process_mem(proc_logs, use_pss):
"""Collect per-process memory samples. Returns {name: {metric: [values_per_sample_in_MB]}}."""
by_proc = defaultdict(lambda: defaultdict(list))
for msg in proc_logs:
sample = defaultdict(lambda: defaultdict(float))
for proc in msg.procLog.procs:
name = get_proc_name(proc)
sample[name]['rss'] += proc.memRss / MB
if use_pss:
sample[name]['pss'] += proc.memPss / MB
sample[name]['pss_anon'] += proc.memPssAnon / MB
sample[name]['pss_shmem'] += proc.memPssShmem / MB
for name, metrics in sample.items():
for metric, val in metrics.items():
by_proc[name][metric].append(val)
return by_proc
def _has_pss_detail(by_proc) -> bool:
"""Check if any process has non-zero pss_anon/pss_shmem (unavailable on some kernels)."""
return any(sum(v.get('pss_anon', [])) > 0 or sum(v.get('pss_shmem', [])) > 0 for v in by_proc.values())
def process_table_rows(by_proc, total_mb, use_pss, show_detail):
"""Build table rows. Returns (rows, total_row)."""
mem_key = 'pss' if use_pss else 'rss'
rows = []
for name in sorted(by_proc, key=lambda n: np.mean(by_proc[n][mem_key]), reverse=True):
m = by_proc[name]
vals = m[mem_key]
avg = round(np.mean(vals))
row = [name, f"{avg} MB", f"{round(np.max(vals))} MB", f"{round(pct(avg, total_mb), 1)}%"]
if show_detail:
row.append(f"{round(np.mean(m['pss_anon']))} MB")
row.append(f"{round(np.mean(m['pss_shmem']))} MB")
rows.append(row)
# Total row
total_row = None
if by_proc:
max_samples = max(len(v[mem_key]) for v in by_proc.values())
totals = []
for i in range(max_samples):
s = sum(v[mem_key][i] for v in by_proc.values() if i < len(v[mem_key]))
totals.append(s)
avg_total = round(np.mean(totals))
total_row = ["TOTAL", f"{avg_total} MB", f"{round(np.max(totals))} MB", f"{round(pct(avg_total, total_mb), 1)}%"]
if show_detail:
total_row.append(f"{round(sum(np.mean(v['pss_anon']) for v in by_proc.values()))} MB")
total_row.append(f"{round(sum(np.mean(v['pss_shmem']) for v in by_proc.values()))} MB")
return rows, total_row
def print_process_tables(op_procs, other_procs, total_mb, use_pss):
all_procs = {**op_procs, **other_procs}
show_detail = use_pss and _has_pss_detail(all_procs)
header = ["process", "avg", "max", "%"]
if show_detail:
header += ["anon", "shmem"]
op_rows, op_total = process_table_rows(op_procs, total_mb, use_pss, show_detail)
# filter other: >5MB avg and not bare interpreter paths (test infra noise)
other_filtered = {n: v for n, v in other_procs.items()
if np.mean(v['pss' if use_pss else 'rss']) > 5.0
and os.path.basename(n.split()[0]) not in ('python', 'python3')}
other_rows, other_total = process_table_rows(other_filtered, total_mb, use_pss, show_detail)
rows = op_rows
if op_total:
rows.append(op_total)
if other_rows:
sep_width = len(header)
rows.append([""] * sep_width)
rows.extend(other_rows)
if other_total:
other_total[0] = "TOTAL (other)"
rows.append(other_total)
metric = "PSS (no shared double-count)" if use_pss else "RSS (includes shared, overcounts)"
print(f"\n-- Per-Process Memory: {metric} --")
print(tabulate(rows, header, **TABULATE_OPTS))
def print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss):
last = proc_logs[-1].procLog.mem
used = (last.total - last.available) / MB
shared = last.shared / MB
cached_buf = (last.buffers + last.cached) / MB - shared # shared (MSGQ) is in Cached; separate it
msgq = shared
mem_key = 'pss' if use_pss else 'rss'
op_total = sum(v[mem_key][-1] for v in op_procs.values()) if op_procs else 0
other_total = sum(v[mem_key][-1] for v in other_procs.values()) if other_procs else 0
proc_sum = op_total + other_total
remainder = used - (cached_buf + msgq) - proc_sum
if not use_pss:
# RSS double-counts shared; add back once to partially correct
remainder += shared
header = ["", "MB", "%", ""]
label = "PSS" if use_pss else "RSS*"
rows = [
["Used (total - avail)", f"{used:.0f}", f"{pct(used, total_mb):.1f}", "memory in use by the system"],
[" Cached + Buffers", f"{cached_buf:.0f}", f"{pct(cached_buf, total_mb):.1f}", "pagecache + fs metadata, reclaimable"],
[" MSGQ (shared)", f"{msgq:.0f}", f"{pct(msgq, total_mb):.1f}", "/dev/shm tmpfs, also in process PSS"],
[f" openpilot {label}", f"{op_total:.0f}", f"{pct(op_total, total_mb):.1f}", "sum of openpilot process memory"],
[f" other {label}", f"{other_total:.0f}", f"{pct(other_total, total_mb):.1f}", "sum of non-openpilot process memory"],
[" kernel/ION/GPU", f"{remainder:.0f}", f"{pct(remainder, total_mb):.1f}", "slab, ION/DMA-BUF, GPU, page tables"],
]
note = "" if use_pss else " (*RSS overcounts shared mem)"
print(f"\n-- Memory Accounting (last sample){note} --")
print(tabulate(rows, header, tablefmt="simple_grid", stralign="right"))
def print_report(proc_logs, device_states=None):
"""Print full memory analysis report. Can be called from tests or CLI."""
if not proc_logs:
print("No procLog messages found")
return
print(f"{len(proc_logs)} procLog samples, {len(device_states or [])} deviceState samples")
use_pss = has_pss(proc_logs)
if not use_pss:
print(" (no PSS data — re-record with updated proclogd for accurate numbers)")
total_mb = print_summary(proc_logs, device_states or [])
by_proc = collect_per_process_mem(proc_logs, use_pss)
op_procs = {n: v for n, v in by_proc.items() if is_openpilot_proc(n)}
other_procs = {n: v for n, v in by_proc.items() if not is_openpilot_proc(n)}
print_process_tables(op_procs, other_procs, total_mb, use_pss)
print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze memory usage from route logs")
parser.add_argument("route", nargs="?", default=None, help="route ID or local rlog path")
parser.add_argument("--demo", action="store_true", help=f"use demo route ({DEMO_ROUTE})")
args = parser.parse_args()
if args.demo:
route = DEMO_ROUTE
elif args.route:
route = args.route
else:
parser.error("provide a route or use --demo")
print(f"Reading logs from: {route}")
proc_logs = []
device_states = []
for msg in LogReader(route):
if msg.which() == 'procLog':
proc_logs.append(msg)
elif msg.which() == 'deviceState':
device_states.append(msg)
print_report(proc_logs, device_states)
+1 -1
View File
@@ -59,7 +59,7 @@ class ModelState:
self.tensor_inputs['input_img'] = Tensor(self.frame.buffer_from_cl(input_img_cl).reshape(self.input_shapes['input_img']), dtype=dtypes.uint8).realize()
output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy()
output = self.model_run(**self.tensor_inputs).numpy().flatten()
t2 = time.perf_counter()
return output, t2 - t1
+1 -1
View File
@@ -217,7 +217,7 @@ class ModelState(ModelStateBase):
self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k]
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
self.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten()
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:35e4a5d4c4d481f915e42358af4665b2c92b8f5c1efd1c0731f21b876ad1d856
size 6954249
oid sha256:3446bf8b22e50e47669a25bf32460ae8baf8547037f346753e19ecbfcf6d4e59
size 6954368
+21 -4
View File
@@ -35,7 +35,14 @@ class DRIVER_MONITOR_SETTINGS:
self._EYE_THRESHOLD = 0.65
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
self._PHONE_THRESH = 0.5
self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4
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
@@ -145,10 +152,11 @@ class DriverMonitoring:
# init driver status
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.phone_prob = 0.
self.always_on = always_on
self.distracted_types = []
@@ -249,7 +257,12 @@ class DriverMonitoring:
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
distracted_types.append(DistractedType.DISTRACTED_BLINK)
if self.phone_prob > self.settings._PHONE_THRESH:
if self.phone.prob_calibrated:
using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \
* self.settings._PHONE_THRESH2
else:
using_phone = self.phone.prob > self.settings._PHONE_THRESH
if using_phone:
distracted_types.append(DistractedType.DISTRACTED_PHONE)
return distracted_types
@@ -288,7 +301,7 @@ class DriverMonitoring:
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.phone_prob = driver_data.phoneProb
self.phone.prob = driver_data.phoneProb
self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
@@ -302,9 +315,11 @@ class DriverMonitoring:
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
self.phone.prob_offseter.push_and_update(self.phone.prob)
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
@@ -410,6 +425,8 @@ class DriverMonitoring:
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
"phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(),
"phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n,
"stepChange": self.step_change,
"awarenessActive": self.awareness_active,
"awarenessPassive": self.awareness_passive,
+4 -4
View File
@@ -6,16 +6,16 @@ import time
import signal
import subprocess
from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH
from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, FW_PATH
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
from openpilot.common.swaglog import cloudlog
def get_expected_signature(panda: Panda) -> bytes:
def get_expected_signature() -> bytes:
try:
fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn)
fn = os.path.join(FW_PATH, McuType.H7.config.app_fn)
return Panda.get_signature_from_firmware(fn)
except Exception:
cloudlog.exception("Error computing expected signature")
@@ -35,7 +35,7 @@ def flash_panda(panda_serial: str) -> Panda:
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
return panda
fw_signature = get_expected_signature(panda)
fw_signature = get_expected_signature()
internal_panda = panda.is_internal()
panda_version = "bootstub" if panda.bootstub else panda.get_version()
-5
View File
@@ -304,11 +304,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
},
EventName.stockLkas: {
ET.PERMANENT: Alert(
"Stock LKAS: Lane Departure Detected",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.),
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
},
+1 -2
View File
@@ -22,7 +22,7 @@ Currently the following processes are tested:
### Usage
```
Usage: test_processes.py [-h] [--whitelist-procs PROCS] [--whitelist-cars CARS] [--blacklist-procs PROCS]
[--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs] [--upload-only]
[--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs]
Regression test to identify changes in a process's output
optional arguments:
-h, --help show this help message and exit
@@ -33,7 +33,6 @@ optional arguments:
--ignore-fields IGNORE_FIELDS Extra fields or msgs to ignore (e.g. driverMonitoringState.events)
--ignore-msgs IGNORE_MSGS Msgs to ignore (e.g. onroadEvents)
--update-refs Updates reference logs using current commit
--upload-only Skips testing processes and uploads logs from previous test run
```
## Forks
@@ -9,7 +9,7 @@ from itertools import zip_longest
import matplotlib.pyplot as plt
import numpy as np
from tabulate import tabulate
from openpilot.common.utils import tabulate
from openpilot.common.git import get_commit
from openpilot.system.hardware import PC
-1
View File
@@ -1 +0,0 @@
67f3daf309dc6cbb6844fcbaeb83e6596637e551
+21 -96
View File
@@ -9,14 +9,13 @@ from typing import Any
from opendbc.car.car_helpers import interface_names
from openpilot.common.git import get_commit
from openpilot.tools.lib.openpilotci import get_url, upload_file
from openpilot.tools.lib.openpilotci import get_url
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, replay_process, \
check_most_messages_valid
from openpilot.tools.lib.filereader import FileReader
from openpilot.tools.lib.logreader import LogReader, save_log
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
from openpilot.tools.lib.url_file import URLFile
source_segments = [
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
@@ -66,46 +65,17 @@ segments = [
# dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = ["mock", "body", "psa"]
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
def preserve_only_specified_files_from_ref_commit(*commits_to_keep):
"""Keep only files in fakedata that contain any of the specified commit hashes."""
removed = 0
for f in os.listdir(FAKEDATA):
if not any(commit in f for commit in commits_to_keep):
os.remove(os.path.join(FAKEDATA, f))
removed += 1
if removed > 0:
print(f"Removed {removed} old files from {FAKEDATA}")
def handle_output_file(cur_log_fn, local):
"""Handle the output file based on whether we're using remote or local storage."""
assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}"
if local:
os.system(f"git add '{os.path.realpath(cur_log_fn)}'")
else:
upload_file(cur_log_fn, os.path.basename(cur_log_fn))
os.remove(cur_log_fn)
def run_test_process(data):
segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data
res = None
if not args.upload_only:
lr = LogReader.from_bytes(lr_dat)
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
# save logs so we can upload when updating refs
save_log(cur_log_fn, log_msgs)
if args.update_refs or args.upload_only:
print(f'Processing: {os.path.basename(cur_log_fn)}')
handle_output_file(cur_log_fn, args.local)
lr = LogReader.from_bytes(lr_dat)
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
# save logs so we can update refs
save_log(cur_log_fn, log_msgs)
return (segment, cfg.proc_name, res)
@@ -144,27 +114,6 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non
return str(e), log_msgs
def finalize_git_updates(cur_commit, ref_commit_fn):
"""Finalize git updates and create commit."""
try:
# Add all new files first
os.system(f"git add {os.path.realpath(ref_commit_fn)}")
os.system(f"git add {os.path.realpath(FAKEDATA)}/*.zst")
# Clean up old files - keep only new ref files since they're becoming the reference
preserve_only_specified_files_from_ref_commit(cur_commit)
# Add the deletions to git
os.system(f"git add -u {os.path.realpath(FAKEDATA)}")
# Create the commit
commit_msg = f"test_processes: update ref logs to {cur_commit[:7]}"
os.system(f'git commit -m "{commit_msg}"')
print("Successfully committed reference log updates")
except Exception as e:
print(f"Failed to commit changes: {e}")
if __name__ == "__main__":
all_cars = {car for car, _ in segments}
all_procs = {cfg.proc_name for cfg in CONFIGS if cfg.proc_name not in EXCLUDED_PROCS}
@@ -186,10 +135,6 @@ if __name__ == "__main__":
help="Msgs to ignore (e.g. carEvents)")
parser.add_argument("--update-refs", action="store_true",
help="Updates reference logs using current commit")
parser.add_argument("--upload-only", action="store_true",
help="Skips testing processes and uploads logs from previous test run")
parser.add_argument("--local", action="store_true",
help="Use local git/ storage instead of remote (Azure for Comma)")
parser.add_argument("-j", "--jobs", type=int, default=max(cpu_count - 2, 1),
help="Max amount of parallel jobs")
args = parser.parse_args()
@@ -199,33 +144,21 @@ if __name__ == "__main__":
tested_cars = {c.upper() for c in tested_cars}
full_test = (tested_procs == all_procs) and (tested_cars == all_cars) and all(len(x) == 0 for x in (args.ignore_fields, args.ignore_msgs))
upload = args.update_refs or args.upload_only
os.makedirs(os.path.dirname(FAKEDATA), exist_ok=True)
if upload:
if args.update_refs:
assert full_test, "Need to run full test when updating refs"
try:
with open(REF_COMMIT_FN) as f:
ref_commit = f.read().strip()
except FileNotFoundError:
print("Couldn't find reference commit")
sys.exit(1)
ref_commit = URLFile(BASE_URL + "ref_commit", cache=False).read().decode().strip()
cur_commit = get_commit()
if not cur_commit:
raise Exception("Couldn't get current commit")
# Could be set as default in args, but wanted to be more explicit on the flow.
if upload and not args.local and not IS_AZURE_TOKEN_DEFINED:
print("***** Warning: local/git run was used by default since AZURE_TOKEN was NOT found on the env variables! *****")
args.local = True
# Clean up old files before starting
if upload and args.local:
print("***** Cleaning up old fakedata for local/git tracked refs *****")
preserve_only_specified_files_from_ref_commit(cur_commit, ref_commit)
print(f"***** testing against commit {ref_commit} *****")
# check to make sure all car brands are tested
@@ -235,12 +168,11 @@ if __name__ == "__main__":
log_paths: defaultdict[str, dict[str, dict[str, str]]] = defaultdict(lambda: defaultdict(dict))
with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool:
if not args.upload_only:
download_segments = [seg for car, seg in segments if car in tested_cars]
log_data: dict[str, LogReader] = {}
p1 = pool.map(get_log_data, download_segments)
for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
log_data[segment] = lr
download_segments = [seg for car, seg in segments if car in tested_cars]
log_data: dict[str, LogReader] = {}
p1 = pool.map(get_log_data, download_segments)
for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
log_data[segment] = lr
pool_args: Any = []
for car_brand, segment in segments:
@@ -255,15 +187,15 @@ if __name__ == "__main__":
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
continue
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst".replace("|", "_"))
if args.update_refs: # reference logs will not exist if routes were just regenerated
ref_log_path = get_url(*segment.rsplit("--", 1,), "rlog.zst")
route, seg_num = segment.rsplit("--", 1)
ref_log_path = get_url(route, seg_num, "rlog.zst")
else:
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst")
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst".replace("|", "_"))
ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn)
dat = None if args.upload_only else log_data[segment]
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat))
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, log_data[segment]))
log_paths[segment][cfg.proc_name]['ref'] = ref_log_path
log_paths[segment][cfg.proc_name]['new'] = cur_log_fn
@@ -271,19 +203,16 @@ if __name__ == "__main__":
results: Any = defaultdict(dict)
p2 = pool.map(run_test_process, pool_args)
for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)):
if not args.upload_only:
results[segment][proc] = result
results[segment][proc] = result
diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit)
if not upload:
if not args.update_refs:
with open(os.path.join(PROC_REPLAY_DIR, "diff.txt"), "w") as f:
f.write(diff_long)
print(diff_short)
if failed:
print("TEST FAILED")
print("\n\nTo push the new reference logs for this commit run:")
print("./test_processes.py --upload-only")
else:
print("TEST SUCCEEDED")
@@ -292,8 +221,4 @@ if __name__ == "__main__":
f.write(cur_commit)
print(f"\n\nUpdated reference logs for commit: {cur_commit}")
# Only do git operations if we're in local mode
if upload and args.local:
finalize_git_updates(cur_commit, REF_COMMIT_FN)
sys.exit(int(failed))
+6 -3
View File
@@ -8,7 +8,7 @@ import time
import numpy as np
from collections import Counter, defaultdict
from pathlib import Path
from tabulate import tabulate
from openpilot.common.utils import tabulate
from cereal import log
import cereal.messaging as messaging
@@ -56,7 +56,7 @@ PROCS = {
"selfdrive.ui.soundd": 3.0,
"selfdrive.ui.feedback.feedbackd": 1.0,
"selfdrive.monitoring.dmonitoringd": 4.0,
"system.proclogd": 3.0,
"system.proclogd": 7.0,
"system.logmessaged": 1.0,
"system.tombstoned": 0,
"system.journald": 1.0,
@@ -282,9 +282,12 @@ class TestOnroad:
print("\n------------------------------------------------")
print("--------------- Memory Usage -------------------")
print("------------------------------------------------")
from openpilot.selfdrive.debug.mem_usage import print_report
print_report(self.msgs['procLog'], self.msgs['deviceState'])
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]]
print("Overall memory usage: ", mems)
print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode())
# check for big leaks. note that memory usage is
+1 -1
View File
@@ -50,7 +50,7 @@ class MiciMainLayout(Widget):
self._alerts_layout,
self._home_layout,
self._onroad_layout,
], spacing=0, pad_start=0, pad_end=0)
], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False)
self._scroller.set_reset_scroll_at_show(False)
# Disable scrolling when onroad is interacting with bookmark
+8 -26
View File
@@ -1,6 +1,5 @@
import os
import threading
import json
import pyray as rl
from enum import IntEnum
from collections.abc import Callable
@@ -11,7 +10,7 @@ from openpilot.common.time_helpers import system_time_valid
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide
@@ -121,6 +120,9 @@ class PairBigButton(BigButton):
def __init__(self):
super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60))
def _get_label_font_size(self):
return 64
def _update_state(self):
if ui_state.prime_state.is_paired():
self.set_text("paired")
@@ -222,7 +224,7 @@ class UpdateOpenpilotBigButton(BigButton):
if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT:
self.set_rotate_icon(False)
self.set_value("updater failed to respond")
self.set_value("updater failed\nto respond")
self._state = UpdaterState.IDLE
self._hide_value_t = rl.get_time()
@@ -303,30 +305,14 @@ class DeviceLayoutMici(NavWidget):
self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66))
self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off"))
self._load_languages()
def language_callback():
def selected_language_callback():
selected_language = dlg.get_selected_option()
ui_state.params.put("LanguageSetting", self._languages[selected_language])
current_language_name = ui_state.params.get("LanguageSetting")
current_language = next(name for name, lang in self._languages.items() if lang == current_language_name)
dlg = BigMultiOptionDialog(list(self._languages), default=current_language, right_btn_callback=selected_language_callback)
gui_app.set_modal_overlay(dlg)
# lang_button = BigButton("change language", "", "icons_mici/settings/device/language.png")
# lang_button.set_click_callback(language_callback)
regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png")
regulatory_btn.set_click_callback(self._on_regulatory)
driver_cam_btn = BigButton("driver camera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn.set_click_callback(self._show_driver_camera)
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
review_training_guide_btn = BigButton("review training guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn.set_click_callback(self._on_review_training_guide)
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
@@ -353,7 +339,7 @@ class DeviceLayoutMici(NavWidget):
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None))
gui_app.set_modal_overlay(self._fcc_dialog)
def _offroad_transition(self):
self._power_off_btn.set_visible(ui_state.is_offroad())
@@ -371,10 +357,6 @@ class DeviceLayoutMici(NavWidget):
self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None))
def _load_languages(self):
with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f:
self._languages = json.load(f)
def show_event(self):
super().show_event()
self._scroller.show_event()
@@ -3,8 +3,8 @@ 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, BigParamControl, BigCircleToggle
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
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
@@ -39,8 +39,7 @@ class NetworkLayoutMici(NavWidget):
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigCircleToggle("icons_mici/tethering_short.png", toggle_callback=tethering_toggle_callback,
icon_size=(82, 82), icon_offset=(0, 12))
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
@@ -56,9 +55,6 @@ class NetworkLayoutMici(NavWidget):
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)
@@ -74,8 +70,13 @@ class NetworkLayoutMici(NavWidget):
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))
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
# ******** Advanced settings ********
# ******** Roaming toggle ********
@@ -90,7 +91,7 @@ class NetworkLayoutMici(NavWidget):
# Main scroller ----------------------------------
self._scroller = Scroller([
wifi_button,
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
@@ -99,7 +100,6 @@ class NetworkLayoutMici(NavWidget):
self._apn_btn,
self._cellular_metered_btn,
# */
self._ip_address_btn,
], snap_items=False)
# Set initial config
@@ -158,8 +158,22 @@ class NetworkLayoutMici(NavWidget):
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 wi-fi button with ssid and ip address
# TODO: make sure we handle hidden ssids
connected_network = next((network for network in networks if network.is_connected), None)
self._wifi_button.set_text(normalize_ssid(connected_network.ssid) if connected_network is not None else "wi-fi")
self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected")
if connected_network is not None:
strength = WifiIcon.get_strength_icon_idx(connected_network.strength)
if strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 1:
strength_icon = self._wifi_medium_txt
else:
strength_icon = self._wifi_low_txt
self._wifi_button.set_icon(strength_icon)
else:
self._wifi_button.set_icon(self._wifi_slash_txt)
# Update network metered
self._network_metered_btn.set_value(
@@ -50,12 +50,16 @@ class WifiIcon(Widget):
def set_scale(self, scale: float):
self._scale = scale
@staticmethod
def get_strength_icon_idx(strength: int) -> int:
return round(strength / 100 * 2)
def _render(self, _):
if self._network is None:
return
# Determine which wifi strength icon to use
strength = round(self._network.strength / 100 * 2)
strength = self.get_strength_icon_idx(self._network.strength)
if strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 1:
@@ -314,7 +318,7 @@ class WifiUIMici(BigMultiOptionDialog):
INACTIVITY_TIMEOUT = 1
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
super().__init__([], None, None, right_btn_callback=None)
super().__init__([], None)
# Set up back navigation
self.set_back_callback(back_callback)
+10 -5
View File
@@ -30,22 +30,27 @@ class PanelInfo:
instance: Widget
class SettingsBigButton(BigButton):
def _get_label_font_size(self):
return 64
class SettingsLayout(NavWidget):
def __init__(self):
super().__init__()
self._params = Params()
self._current_panel = None # PanelType.DEVICE
toggles_btn = BigButton("toggles", "", "icons_mici/settings.png")
toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES))
network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK))
device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE))
developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER))
firehose_btn = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE))
self._scroller = Scroller([
@@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets import Widget
from openpilot.common.filter_simple import BounceFilter
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler
from enum import IntEnum
@@ -169,6 +169,7 @@ class AugmentedRoadView(CameraView):
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
# debug
self._pm = messaging.PubMaster(['uiDebug'])
@@ -221,8 +222,11 @@ class AugmentedRoadView(CameraView):
# Draw all UI overlays
self._model_renderer.render(self._content_rect)
# Fade out bottom of overlays for looks
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE)
# Fade out bottom of overlays for looks (only when engaged)
fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
if fade_alpha > 1e-2:
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0,
rl.Color(255, 255, 255, int(255 * fade_alpha)))
alert_to_render, not_animating_out = self._alert_renderer.will_render()
+8 -3
View File
@@ -12,7 +12,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
@@ -51,9 +51,10 @@ class LeadVehicle:
fill_alpha: int = 0
class ModelRenderer(Widget):
class ModelRenderer(Widget, ModelRendererSP):
def __init__(self):
super().__init__()
Widget.__init__(self)
ModelRendererSP.__init__(self)
self._longitudinal_control = False
self._experimental_mode = False
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
@@ -340,6 +341,10 @@ class ModelRenderer(Widget):
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
self._blend_filter.update(int(allow_throttle))
if ui_state.rainbow_path:
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
return
if self._experimental_mode:
# Draw with acceleration coloring
if ui_state.status == UIStatus.DISENGAGED:
+76 -109
View File
@@ -3,9 +3,8 @@ from typing import Union
from enum import Enum
from collections.abc import Callable
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import DO_ZOOM
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.common.filter_simple import BounceFilter
@@ -18,6 +17,7 @@ SCROLLING_SPEED_PX_S = 50
COMPLICATION_SIZE = 36
LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9))
LABEL_HORIZONTAL_PADDING = 40
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255)
PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07
@@ -52,6 +52,12 @@ class BigCircleButton(Widget):
def set_enable_pressed_state(self, pressed: bool):
self._press_state_enabled = pressed
def _draw_content(self, btn_y: float):
# draw icon
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0],
btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color)
def _render(self, _):
# draw background
txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg
@@ -65,10 +71,7 @@ class BigCircleButton(Widget):
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
# draw icon
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0]),
int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), icon_color)
self._draw_content(btn_y)
class BigCircleToggle(BigCircleButton):
@@ -93,48 +96,41 @@ class BigCircleToggle(BigCircleButton):
if self._toggle_callback:
self._toggle_callback(self._checked)
def _render(self, _):
super()._render(_)
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
# draw status icon
rl.draw_texture(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
int(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2),
int(self._rect.y + 5), rl.WHITE)
rl.draw_texture_ex(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2, btn_y + 5),
0, 1.0, rl.WHITE)
class BigButton(Widget):
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64)):
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64),
scroll: bool = False):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 402, 180))
self.text = text
self.value = value
self._icon_size = icon_size
self._scroll = scroll
self.set_icon(icon)
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._rotate_icon_t: float | None = None
self._label_font = gui_app.font(FontWeight.DISPLAY)
self._value_font = gui_app.font(FontWeight.ROMAN)
self._label = MiciLabel(text, font_size=self._get_label_font_size(), width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
font_weight=FontWeight.DISPLAY, color=LABEL_COLOR,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
self._sub_label = MiciLabel(value, font_size=COMPLICATION_SIZE, width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
font_weight=FontWeight.ROMAN, color=COMPLICATION_GREY,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
line_height=0.9)
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._update_label_layout()
self._load_images()
# internal state
self._scroll_offset = 0 # in pixels
self._needs_scroll = measure_text_cached(self._label_font, text, self._get_label_font_size()).x + 25 > self._rect.width
self._scroll_timer = 0
self._scroll_state = ScrollState.PRE_SCROLL
def set_icon(self, icon: Union[str, rl.Texture]):
self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon
@@ -149,28 +145,33 @@ class BigButton(Widget):
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180)
def _width_hint(self) -> int:
# Single line if scrolling, so hide behind icon if exists
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - icon_size)
def _get_label_font_size(self):
if len(self.text) < 12:
font_size = 64
elif len(self.text) < 17:
font_size = 48
elif len(self.text) < 20:
font_size = 42
if len(self.text) <= 18:
return 48
else:
font_size = 36
return 42
def _update_label_layout(self):
self._label.set_font_size(self._get_label_font_size())
if self.value:
font_size -= 20
return font_size
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
else:
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
def set_text(self, text: str):
self.text = text
self._label.set_text(text)
self._update_label_layout()
def set_value(self, value: str):
self.value = value
self._sub_label.set_text(value)
self._update_label_layout()
def get_value(self) -> str:
return self.value
@@ -178,37 +179,35 @@ class BigButton(Widget):
def get_text(self):
return self.text
def _update_state(self):
# hold on text for a bit, scroll, hold again, reset
if self._needs_scroll:
"""`dt` should be seconds since last frame (rl.get_frame_time())."""
# TODO: this comment is generated by GPT, prob wrong and misused
dt = rl.get_frame_time()
def _draw_content(self, btn_y: float):
# LABEL ------------------------------------------------------------------
label_x = self._rect.x + LABEL_HORIZONTAL_PADDING
self._scroll_timer += dt
if self._scroll_state == ScrollState.PRE_SCROLL:
if self._scroll_timer < 0.5:
return
self._scroll_state = ScrollState.SCROLLING
self._scroll_timer = 0
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
self._label.set_color(label_color)
label_rect = rl.Rectangle(label_x, btn_y + LABEL_VERTICAL_PADDING, self._width_hint(),
self._rect.height - LABEL_VERTICAL_PADDING * 2)
self._label.render(label_rect)
elif self._scroll_state == ScrollState.SCROLLING:
self._scroll_offset -= SCROLLING_SPEED_PX_S * dt
# reset when text has completely left the button + 50 px gap
# TODO: use global constant for 30+30 px gap
# TODO: add std Widget padding option integrated into the self._rect
full_len = measure_text_cached(self._label_font, self.text, self._get_label_font_size()).x + 30 + 30
if self._scroll_offset < (self._rect.width - full_len):
self._scroll_state = ScrollState.POST_SCROLL
self._scroll_timer = 0
if self.value:
label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING
sub_label_height = self._sub_label.get_content_height(self._width_hint())
sub_label_rect = rl.Rectangle(label_x, label_y - sub_label_height, self._width_hint(), sub_label_height)
self._sub_label.render(sub_label_rect)
elif self._scroll_state == ScrollState.POST_SCROLL:
# wait for a bit before starting to scroll again
if self._scroll_timer < 0.75:
return
self._scroll_state = ScrollState.PRE_SCROLL
self._scroll_timer = 0
self._scroll_offset = 0
# ICON -------------------------------------------------------------------
if self._txt_icon:
rotation = 0
if self._rotate_icon_t is not None:
rotation = (rl.get_time() - self._rotate_icon_t) * 180
# draw top right with 30px padding
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
y = btn_y + 30 + self._txt_icon.height / 2
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height)
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
def _render(self, _):
# draw _txt_default_bg
@@ -223,33 +222,7 @@ class BigButton(Widget):
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
# LABEL ------------------------------------------------------------------
lx = self._rect.x + LABEL_HORIZONTAL_PADDING
ly = btn_y + self._rect.height - 33 # - 40# - self._get_label_font_size() / 2
if self.value:
self._sub_label.set_position(lx, ly)
ly -= self._sub_label.font_size + 9
self._sub_label.render()
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
self._label.set_color(label_color)
self._label.set_position(lx, ly)
self._label.render()
# ICON -------------------------------------------------------------------
if self._txt_icon:
rotation = 0
if self._rotate_icon_t is not None:
rotation = (rl.get_time() - self._rotate_icon_t) * 180
# drop top right with 30px padding
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
y = self._rect.y + 30 + self._txt_icon.height / 2
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(int(x), int(y), self._txt_icon.width, self._txt_icon.height)
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
self._draw_content(btn_y)
class BigToggle(BigButton):
@@ -258,8 +231,6 @@ class BigToggle(BigButton):
self._checked = initial_state
self._toggle_callback = toggle_callback
self._label.set_font_size(48)
def _load_images(self):
super()._load_images()
self._txt_enabled_toggle = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 84, 66)
@@ -277,15 +248,15 @@ class BigToggle(BigButton):
def _draw_pill(self, x: float, y: float, checked: bool):
# draw toggle icon top right
if checked:
rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE)
rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE)
else:
rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE)
rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE)
def _render(self, _):
super()._render(_)
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
y = self._rect.y
y = btn_y
self._draw_pill(x, y, self._checked)
@@ -297,15 +268,10 @@ class BigMultiToggle(BigToggle):
self._options = options
self._select_callback = select_callback
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
# TODO: why isn't this automatic?
self._label.set_font_size(self._get_label_font_size())
self.set_value(self._options[0])
def _get_label_font_size(self):
font_size = super()._get_label_font_size()
return font_size - 6
def _width_hint(self) -> int:
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
@@ -315,13 +281,14 @@ class BigMultiToggle(BigToggle):
if self._select_callback:
self._select_callback(self.value)
def _render(self, _):
BigButton._render(self, _)
def _draw_content(self, btn_y: float):
# don't draw pill from BigToggle
BigButton._draw_content(self, btn_y)
checked_idx = self._options.index(self.value)
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
y = self._rect.y
y = btn_y
for i in range(len(self._options)):
self._draw_pill(x, y, checked_idx == i)
+17 -37
View File
@@ -14,7 +14,6 @@ from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
DEBUG = False
@@ -22,32 +21,17 @@ PADDING = 20
class BigDialogBase(NavWidget, abc.ABC):
def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None):
def __init__(self):
super().__init__()
self._ret = DialogResult.NO_ACTION
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL))
self._right_btn = None
if right_btn:
def right_btn_callback_wrapper():
gui_app.set_modal_overlay(None)
if right_btn_callback:
right_btn_callback()
self._right_btn = SideButton(right_btn)
self._right_btn.set_click_callback(right_btn_callback_wrapper)
# move to right side
self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width
def _render(self, _) -> DialogResult:
"""
Allows `gui_app.set_modal_overlay(BigDialog(...))`.
The overlay runner keeps calling until result != NO_ACTION.
"""
if self._right_btn:
self._right_btn.set_position(self._right_btn._rect.x, self._rect.y)
self._right_btn.render()
return self._ret
@@ -55,10 +39,8 @@ class BigDialogBase(NavWidget, abc.ABC):
class BigDialog(BigDialogBase):
def __init__(self,
title: str,
description: str,
right_btn: str | None = None,
right_btn_callback: Callable | None = None):
super().__init__(right_btn, right_btn_callback)
description: str):
super().__init__()
self._title = title
self._description = description
@@ -70,8 +52,6 @@ class BigDialog(BigDialogBase):
# TODO: coming up with these numbers manually is a pain and not scalable
# TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite
max_width = self._rect.width - PADDING * 2
if self._right_btn:
max_width -= self._right_btn._rect.width
title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width)))
title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50)
@@ -139,7 +119,7 @@ class BigInputDialog(BigDialogBase):
default_text: str = "",
minimum_length: int = 1,
confirm_callback: Callable[[str], None] | None = None):
super().__init__(None, None)
super().__init__()
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
font_weight=FontWeight.MEDIUM)
self._keyboard = MiciKeyboard()
@@ -151,7 +131,8 @@ class BigInputDialog(BigDialogBase):
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36)
self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 42, 36)
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62)
self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62)
self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
# rects for top buttons
@@ -186,9 +167,9 @@ class BigInputDialog(BigDialogBase):
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE)
bg_block_margin = 5
text_x = PADDING * 2 + self._enter_img.width + bg_block_margin
text_x = PADDING / 2 + self._enter_img.width + PADDING
text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin,
int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width) - bg_block_margin * 2,
int(self._rect.width - text_x * 2),
int(text_size.y))
# draw text input
@@ -224,7 +205,7 @@ class BigInputDialog(BigDialogBase):
self._backspace_img_alpha.update(255 * bool(text))
if self._backspace_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
rl.draw_texture(self._backspace_img, int(self._rect.width - self._enter_img.width - 15), int(text_field_rect.y), color)
rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), color)
if not text and self._hint_label.text and not candidate_char:
# draw description if no text entered yet and not drawing candidate char
@@ -236,10 +217,12 @@ class BigInputDialog(BigDialogBase):
self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y,
self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height)
self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 255 * 0.35)
if self._enter_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color)
# draw enter button
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_disabled_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
# keyboard goes over everything
self._keyboard.render(self._rect)
@@ -307,9 +290,8 @@ class BigDialogOptionButton(Widget):
class BigMultiOptionDialog(BigDialogBase):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
def __init__(self, options: list[str], default: str | None,
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] | None = None):
super().__init__(right_btn, right_btn_callback=right_btn_callback)
def __init__(self, options: list[str], default: str | None):
super().__init__()
self._options = options
if default is not None:
assert default in options
@@ -322,8 +304,6 @@ class BigMultiOptionDialog(BigDialogBase):
self._can_click = True
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
if self._right_btn is not None:
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
for option in options:
self._scroller.add_widget(BigDialogOptionButton(option))
-31
View File
@@ -1,31 +0,0 @@
import pyray as rl
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import gui_app
# ---------------------------------------------------------------------------
# Constants extracted from the original Qt style
# ---------------------------------------------------------------------------
# TODO: this should be corrected, but Scroller relies on this being incorrect :/
WIDTH, HEIGHT = 112, 240
class SideButton(Widget):
def __init__(self, btn_type: str):
super().__init__()
self.type = btn_type
self.set_rect(rl.Rectangle(0, 0, WIDTH, HEIGHT))
# load pre-rendered button images
if btn_type not in ("check", "back"):
btn_type = "back"
btn_img_path = f"icons_mici/buttons/button_side_{btn_type}.png"
btn_img_pressed_path = f"icons_mici/buttons/button_side_{btn_type}_pressed.png"
self._txt_btn, self._txt_btn_back = gui_app.texture(btn_img_path, 100, 224), gui_app.texture(btn_img_pressed_path, 100, 224)
def _render(self, _) -> bool:
x = int(self._rect.x + 12)
y = int(self._rect.y + (self._rect.height - self._txt_btn.height) / 2)
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back,
x, y, rl.WHITE)
return False
@@ -4,27 +4,190 @@ 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.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from enum import IntEnum
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller_tici import Scroller
class PanelType(IntEnum):
CRUISE = 0
SLA = 1
ICBM_DESC = tr_noop("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons " +
"by emulating button presses for limited longitudinal control.")
ICMB_UNAVAILABLE = tr_noop("Intelligent Cruise Button Management is currently unavailable on this platform.")
ICMB_UNAVAILABLE_LONG_AVAILABLE = tr_noop("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.")
ICMB_UNAVAILABLE_LONG_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control is the default longitudinal control for this platform.")
ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments for cruise speed increase/decrease.")
ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.")
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
class CruiseLayout(Widget):
def __init__(self):
super().__init__()
self._current_panel = PanelType.CRUISE
self._speed_limit_layout = SpeedLimitSettingsLayout(lambda: self._set_current_panel(PanelType.CRUISE))
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
self.icbm_toggle = toggle_item_sp(
title=tr("Intelligent Cruise Button Management (ICBM) (Alpha)"),
description="",
param="IntelligentCruiseButtonManagement")
self.scc_v_toggle = toggle_item_sp(
title=tr("Smart Cruise Control - Vision"),
description=tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."),
param="SmartCruiseControlVision")
self.scc_m_toggle = toggle_item_sp(
title=tr("Smart Cruise Control - Map"),
description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
param="SmartCruiseControlMap")
self.custom_acc_toggle = toggle_item_sp(
title=tr("Custom ACC Speed Increments"),
description="",
param="CustomAccIncrementsEnabled",
callback=self._on_custom_acc_toggle)
self.custom_acc_short_increment = option_item_sp(
title=tr("Short Press Increment"),
param="CustomAccShortPressIncrement",
min_value=1, max_value=10, value_change_step=1,
inline=True)
self.custom_acc_long_increment = option_item_sp(
title=tr("Long Press Increment"),
param="CustomAccLongPressIncrement",
value_map={1: 1, 2: 5, 3: 10},
min_value=1, max_value=3, value_change_step=1,
inline=True)
self.sla_settings_button = simple_button_item_sp(
button_text=lambda: tr("Speed Limit"),
button_width=800,
callback=lambda: self._set_current_panel(PanelType.SLA)
)
self.dec_toggle = toggle_item_sp(
title=tr("Enable Dynamic Experimental Control"),
description=tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
param="DynamicExperimentalControl")
items = [
self.icbm_toggle,
self.dec_toggle,
self.scc_v_toggle,
self.scc_m_toggle,
self.custom_acc_toggle,
self.custom_acc_short_increment,
self.custom_acc_long_increment,
self.sla_settings_button,
]
return items
def _render(self, rect):
self._scroller.render(rect)
if self._current_panel == PanelType.SLA:
self._speed_limit_layout.render(rect)
else:
self._scroller.render(rect)
def show_event(self):
self._set_current_panel(PanelType.CRUISE)
self._scroller.show_event()
self.icbm_toggle.show_description(True)
self.custom_acc_toggle.show_description(True)
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
if panel == PanelType.SLA:
self._speed_limit_layout.show_event()
def _update_state(self):
super()._update_state()
if ui_state.CP is not None and ui_state.CP_SP is not None:
has_icbm = ui_state.has_icbm
has_long = ui_state.has_longitudinal_control
if ui_state.CP_SP.intelligentCruiseButtonManagementAvailable and not has_long:
self.icbm_toggle.action_item.set_enabled(ui_state.is_offroad())
self.icbm_toggle.set_description(tr(ICBM_DESC))
else:
ui_state.params.remove("IntelligentCruiseButtonManagement")
self.icbm_toggle.action_item.set_enabled(False)
long_desc = ICMB_UNAVAILABLE
if has_long:
if ui_state.CP.alphaLongitudinalAvailable:
long_desc += " " + ICMB_UNAVAILABLE_LONG_AVAILABLE
else:
long_desc += " " + ICMB_UNAVAILABLE_LONG_UNAVAILABLE
new_desc = "<b>" + tr(long_desc) + "</b>\n\n" + tr(ICBM_DESC)
if self.icbm_toggle.description != new_desc:
self.icbm_toggle.set_description(new_desc)
self.icbm_toggle.show_description(True)
if has_long or has_icbm:
self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
self.dec_toggle.action_item.set_enabled(has_long)
self.scc_v_toggle.action_item.set_enabled(True)
self.scc_m_toggle.action_item.set_enabled(True)
else:
ui_state.params.remove("CustomAccIncrementsEnabled")
ui_state.params.remove("DynamicExperimentalControl")
ui_state.params.remove("SmartCruiseControlVision")
ui_state.params.remove("SmartCruiseControlMap")
self.custom_acc_toggle.action_item.set_enabled(False)
self.dec_toggle.action_item.set_enabled(False)
self.scc_v_toggle.action_item.set_enabled(False)
self.scc_m_toggle.action_item.set_enabled(False)
else:
has_icbm = has_long = False
self.icbm_toggle.action_item.set_enabled(False)
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
show_custom_acc_desc = False
if ui_state.is_offroad():
new_custom_acc_desc = tr(ONROAD_ONLY_DESCRIPTION)
show_custom_acc_desc = True
else:
if has_long or has_icbm:
if has_long and ui_state.CP.pcmCruise:
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
show_custom_acc_desc = True
else:
new_custom_acc_desc = tr(ACC_ENABLED_DESCRIPTION)
else:
new_custom_acc_desc = tr(ACC_NOLONG_DESCRIPTION)
show_custom_acc_desc = True
self.custom_acc_toggle.action_item.set_state(False)
if self.custom_acc_toggle.description != new_custom_acc_desc:
self.custom_acc_toggle.set_description(new_custom_acc_desc)
if show_custom_acc_desc:
self.custom_acc_toggle.show_description(True)
self._on_custom_acc_toggle(self.custom_acc_toggle.action_item.get_state())
def _on_custom_acc_toggle(self, state):
self.custom_acc_short_increment.set_visible(state)
self.custom_acc_long_increment.set_visible(state)
self.custom_acc_short_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled)
self.custom_acc_long_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled)
@@ -0,0 +1,65 @@
"""
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 collections.abc import Callable
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
from openpilot.system.ui.widgets.network import NavButton
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description
SPEED_LIMIT_POLICY_BUTTONS = [tr("Car Only"), tr("Map Only"), tr("Car First"), tr("Map First"), tr("Combined")]
SPEED_LIMIT_POLICY_DESCRIPTIONS = [
tr("Car Only: Use Speed Limit data only from Car"),
tr("Map Only: Use Speed Limit data only from OpenStreetMaps"),
tr("Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"),
tr("Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"),
tr("Combined: Use combined Speed Limit data from Car & OpenStreetMaps")
]
class SpeedLimitPolicyLayout(Widget):
def __init__(self, back_btn_callback: Callable):
super().__init__()
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=False, spacing=0)
def _initialize_items(self):
self._speed_limit_policy = multiple_button_item_sp(
title=lambda: tr("Speed Limit Source"),
description=self._get_policy_description,
buttons=SPEED_LIMIT_POLICY_BUTTONS,
param="SpeedLimitPolicy",
button_width=250,
)
items = [
self._speed_limit_policy
]
return items
@staticmethod
def _get_policy_description():
return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS)
def _render(self, rect):
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40)
self._scroller.render(content_rect)
def show_event(self):
self._scroller.show_event()
self._speed_limit_policy.show_description(True)
@@ -0,0 +1,178 @@
"""
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 collections.abc import Callable
from enum import IntEnum
import pyray as rl
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_policy import SpeedLimitPolicyLayout
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import OffsetType as SpeedLimitOffsetType
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, option_item_sp, simple_button_item_sp, LineSeparatorSP
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import NavButton
from openpilot.system.ui.widgets.scroller_tici import Scroller
SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")]
SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")]
SPEED_LIMIT_MODE_DESCRIPTIONS = [
tr("Off: Disables the Speed Limit functions."),
tr("Information: Displays the current road's speed limit."),
tr("Warning: Provides a warning when exceeding the current road's speed limit."),
tr("Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."),
]
SPEED_LIMIT_OFFSET_DESCRIPTIONS = [
tr("None: No Offset"),
tr("Fixed: Adds a fixed offset [Speed Limit + Offset]"),
tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"),
]
class PanelType(IntEnum):
SETTINGS = 0
POLICY = 1
class SpeedLimitSettingsLayout(Widget):
def __init__(self, back_btn_callback: Callable):
super().__init__()
self._current_panel = PanelType.SETTINGS
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
self._policy_layout = SpeedLimitPolicyLayout(lambda: self._set_current_panel(PanelType.SETTINGS))
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=False, spacing=0)
def _initialize_items(self):
self._speed_limit_mode = multiple_button_item_sp(
title=lambda: tr("Speed Limit"),
description=self._get_mode_description,
buttons=SPEED_LIMIT_MODE_BUTTONS,
param="SpeedLimitMode",
button_width=380,
)
self._source_button = simple_button_item_sp(
button_text=lambda: tr("Customize Source"),
button_width=720,
callback=lambda: self._set_current_panel(PanelType.POLICY)
)
self._speed_limit_offset_type = multiple_button_item_sp(
title=lambda: tr("Speed Limit Offset"),
description="",
buttons=SPEED_LIMIT_OFFSET_TYPE_BUTTONS,
param="SpeedLimitOffsetType",
button_width=450,
)
self._speed_limit_value_offset = option_item_sp(
title="",
param="SpeedLimitValueOffset",
min_value=-30,
max_value=30,
description=self._get_offset_description,
label_callback=self._get_offset_label,
)
items = [
self._speed_limit_mode,
LineSeparatorSP(40),
self._source_button,
LineSeparatorSP(40),
self._speed_limit_offset_type,
self._speed_limit_value_offset
]
return items
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
if panel == PanelType.POLICY:
self._policy_layout.show_event()
@staticmethod
def _get_mode_description():
return get_highlighted_description(ui_state.params, "SpeedLimitMode", SPEED_LIMIT_MODE_DESCRIPTIONS)
@staticmethod
def _get_offset_description():
return get_highlighted_description(ui_state.params, "SpeedLimitOffsetType", SPEED_LIMIT_OFFSET_DESCRIPTIONS)
@staticmethod
def _get_offset_label(value):
offset_type = int(ui_state.params.get("SpeedLimitOffsetType", return_default=True))
unit = tr("km/h") if ui_state.is_metric else tr("mph")
if offset_type == int(SpeedLimitOffsetType.percentage):
return f"{value}%"
elif offset_type == int(SpeedLimitOffsetType.fixed):
return f"{value} {unit}"
return str(value)
def _update_state(self):
super()._update_state()
speed_limit_mode_param = ui_state.params.get("SpeedLimitMode", return_default=True)
if ui_state.CP is not None and ui_state.CP_SP is not None:
brand = ui_state.CP.brand
has_long = ui_state.has_longitudinal_control
has_icbm = ui_state.has_icbm
"""
Speed Limit Assist is available when:
- has_long or has_icbm, and
- is not a release branch or not a disallowed brand, and
- is not always disallwed
"""
sla_disallow_in_release = brand == "tesla" and ui_state.is_sp_release
sla_always_disallow = brand == "rivian"
sla_available = (has_long or has_icbm) and not sla_disallow_in_release and not sla_always_disallow
if not sla_available and speed_limit_mode_param == int(SpeedLimitMode.assist):
ui_state.params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
else:
sla_available = False
if not sla_available:
self._speed_limit_mode.action_item.set_enabled_buttons({
int(SpeedLimitMode.off),
int(SpeedLimitMode.information),
int(SpeedLimitMode.warning),
})
else:
self._speed_limit_mode.action_item.set_enabled_buttons(None)
offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True)
self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off))
def _render(self, rect):
if self._current_panel == PanelType.POLICY:
self._policy_layout.render(rect)
return
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40)
self._scroller.render(content_rect)
def show_event(self):
self._current_panel = PanelType.SETTINGS
self._scroller.show_event()
self._speed_limit_mode.show_description(True)
def hide_event(self):
self._current_panel = PanelType.SETTINGS
self._scroller.hide_event()
@@ -72,6 +72,15 @@ class SteeringLayout(Widget):
description="",
label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}',
)
self._blinker_reengage_delay = option_item_sp(
param="BlinkerLateralReengageDelay",
title=lambda: tr("Post-Blinker Delay"),
min_value=0,
max_value=10,
value_change_step=1,
description=lambda: tr("Delay before lateral control resumes after the turn signal ends."),
label_callback=lambda delay: f'{delay} {"s"}'
)
self._torque_control_toggle = toggle_item_sp(
param="EnforceTorqueControl",
title=lambda: tr("Enforce Torque Lateral Control"),
@@ -96,6 +105,7 @@ class SteeringLayout(Widget):
LineSeparatorSP(40),
self._blinker_control_toggle,
self._blinker_control_options,
self._blinker_reengage_delay,
LineSeparatorSP(40),
self._torque_control_toggle,
self._torque_customization_button,
@@ -128,6 +138,7 @@ class SteeringLayout(Widget):
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())
self._blinker_control_options.set_visible(self._blinker_control_toggle.action_item.get_state())
self._blinker_reengage_delay.set_visible(self._blinker_control_toggle.action_item.get_state())
enforce_torque_enabled = self._torque_control_toggle.action_item.get_state()
nnlc_enabled = self._nnlc_toggle.action_item.get_state()
@@ -9,6 +9,7 @@ import pyray as rl
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import NavButton
@@ -112,7 +113,7 @@ class MadsSettingsLayout(Widget):
if self._mads_limited_settings():
ui_state.params.remove("MadsMainCruiseAllowed")
ui_state.params.put_bool("MadsUnifiedEngagementMode", True)
ui_state.params.put("MadsSteeringMode", 2)
ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE)
self._main_cruise_toggle.action_item.set_enabled(False)
self._main_cruise_toggle.action_item.set_state(False)
@@ -122,9 +123,9 @@ class MadsSettingsLayout(Widget):
self._unified_engagement_toggle.action_item.set_state(True)
self._unified_engagement_toggle.set_description("<b>" + DEFAULT_TO_ON + "</b><br>" + MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)
self._steering_mode.action_item.set_enabled(False)
self._steering_mode.set_description(STATUS_DISENGAGE_ONLY)
self._steering_mode.action_item.set_selected_button(2)
self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE)
self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE})
else:
self._main_cruise_toggle.action_item.set_enabled(True)
self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC)
@@ -133,3 +134,4 @@ class MadsSettingsLayout(Widget):
self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)
self._steering_mode.action_item.set_enabled(True)
self._steering_mode.action_item.set_enabled_buttons(None)
@@ -6,8 +6,14 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import UIStatus
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
LANE_LINE_COLORS_SP = {
UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255),
UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255),
}
class ModelRendererSP:
def __init__(self):
self.rainbow_path = RainbowPath()
@@ -23,7 +23,7 @@ class AugmentedRoadViewSP:
def update_fade_out_bottom_overlay(self, _content_rect):
# Fade out bottom of overlays for looks (only when engaged)
fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
if ui_state.torque_bar and fade_alpha > 1e-2:
if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' and fade_alpha > 1e-2:
# Scale the fade texture to the content rect
rl.draw_texture_pro(self._fade_texture,
rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height),
@@ -19,8 +19,8 @@ from openpilot.system.ui.widgets import Widget
class DeveloperUiRenderer(Widget):
DEV_UI_OFF = 0
DEV_UI_RIGHT = 1
DEV_UI_BOTTOM = 2
DEV_UI_BOTTOM = 1
DEV_UI_RIGHT = 2
DEV_UI_BOTH = 3
BOTTOM_BAR_HEIGHT = 61
@@ -62,10 +62,10 @@ class DeveloperUiRenderer(Widget):
if sm.recv_frame["carState"] < ui_state.started_frame:
return
if self.dev_ui_mode == self.DEV_UI_RIGHT:
self._draw_right_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_BOTTOM:
if self.dev_ui_mode == self.DEV_UI_BOTTOM:
self._draw_bottom_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_RIGHT:
self._draw_right_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_BOTH:
self._draw_right_dev_ui(rect)
self._draw_bottom_dev_ui(rect)
+78 -2
View File
@@ -6,9 +6,8 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel
@@ -17,6 +16,11 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartC
from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController
from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer, UI_CONFIG, FONT_SIZES, COLORS, CRUISE_DISABLED_CHAR
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
class HudRendererSP(HudRenderer):
@@ -32,7 +36,21 @@ class HudRendererSP(HudRenderer):
self.speed_renderer = SpeedRenderer()
self._torque_bar = TorqueBar(scale=3.0, always=True)
self.pcm_cruise_speed: bool = True
self.show_icbm_status: bool = False
self.icbm_active_counter: int = 0
self.speed_cluster: float = 0.0
self.speed_conv: float = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
def _update_state(self) -> None:
if ui_state.sm.recv_frame["carState"] < ui_state.started_frame:
return
if ui_state.CP_SP is not None:
self.pcm_cruise_speed = ui_state.CP_SP.pcmCruiseSpeed
self.speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
self.speed_cluster = ui_state.sm['carState'].cruiseState.speedCluster * self.speed_conv
super()._update_state()
self.road_name_renderer.update()
self.speed_limit_renderer.update()
@@ -41,6 +59,64 @@ class HudRendererSP(HudRenderer):
self.circular_alerts_renderer.update()
self.speed_renderer.update()
def _get_icbm_status(self):
if not self.pcm_cruise_speed and ui_state.sm['carControl'].enabled:
if round(self.set_speed) != round(self.speed_cluster):
self.icbm_active_counter = 3 * gui_app.target_fps # 3 seconds usually
elif self.icbm_active_counter > 0:
self.icbm_active_counter -= 1
else:
self.icbm_active_counter = 0
self.show_icbm_status = self.icbm_active_counter > 0
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
self._get_icbm_status()
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
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)
max_color = COLORS.GREY
set_speed_color = COLORS.DARK_GREY
if self.is_cruise_set:
set_speed_color = COLORS.WHITE
if ui_state.status == UIStatus.ENGAGED:
max_color = COLORS.ENGAGED
elif ui_state.status == UIStatus.DISENGAGED:
max_color = COLORS.DISENGAGED
elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.OVERRIDE
max_str_size = 60 if self.show_icbm_status else 40
max_str_y = 15 if self.show_icbm_status else 27
max_text = str(round(self.speed_cluster)) if self.show_icbm_status else tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, max_str_size).x
rl.draw_text_ex(
self._font_semi_bold,
max_text,
rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + max_str_y),
max_str_size,
0,
max_color,
)
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed))
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
rl.draw_text_ex(
self._font_bold,
set_speed_text,
rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 77),
FONT_SIZES.set_speed,
0,
set_speed_color,
)
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
self.speed_renderer.render(rect)
+15 -59
View File
@@ -72,72 +72,28 @@ class TurnSignalWidget(Widget):
class TurnSignalController:
def __init__(self, config: TurnSignalConfig | None = None):
self._config = config or TurnSignalConfig()
def __init__(self):
self._config = TurnSignalConfig()
self._left_signal = TurnSignalWidget(direction=IconSide.left)
self._right_signal = TurnSignalWidget(direction=IconSide.right)
self._last_icon_side = None
@staticmethod
def _update_signal(signal, blindspot, blinker):
if ui_state.blindspot and blindspot:
signal.activate('blind_spot')
elif ui_state.turn_signals and blinker:
signal.activate('signal')
else:
signal.deactivate()
def update(self):
sm = ui_state.sm
ss = sm['selfdriveState']
CS = ui_state.sm['carState']
event_name = ss.alertType.split('/')[0] if ss.alertType else ''
if event_name == 'preLaneChangeLeft':
self._last_icon_side = IconSide.left
self._left_signal.activate('signal')
self._right_signal.deactivate()
elif event_name == 'preLaneChangeRight':
self._last_icon_side = IconSide.right
self._right_signal.activate('signal')
self._left_signal.deactivate()
elif event_name == 'laneChange':
if self._last_icon_side == IconSide.left:
self._left_signal.activate('signal')
self._right_signal.deactivate()
elif self._last_icon_side == IconSide.right:
self._right_signal.activate('signal')
self._left_signal.deactivate()
elif event_name == 'laneChangeBlocked':
CS = sm['carState']
if CS.leftBlinker:
icon_side = IconSide.left
elif CS.rightBlinker:
icon_side = IconSide.right
else:
icon_side = self._last_icon_side
if icon_side == IconSide.left:
self._left_signal.activate('blind_spot')
self._right_signal.deactivate()
elif icon_side == IconSide.right:
self._right_signal.activate('blind_spot')
self._left_signal.deactivate()
else:
self._last_icon_side = None
CS = sm['carState']
if CS.leftBlindspot:
self._left_signal.activate('blind_spot')
elif CS.leftBlinker:
self._left_signal.activate('signal')
else:
self._left_signal.deactivate()
if CS.rightBlindspot:
self._right_signal.activate('blind_spot')
elif CS.rightBlinker:
self._right_signal.activate('signal')
else:
self._right_signal.deactivate()
self._update_signal(self._left_signal, CS.leftBlindspot, CS.leftBlinker)
self._update_signal(self._right_signal, CS.rightBlindspot, CS.rightBlinker)
def render(self, rect: rl.Rectangle):
if not ui_state.turn_signals:
if not ui_state.turn_signals and not ui_state.blindspot:
return
x = rect.x + rect.width / 2
+10 -6
View File
@@ -26,6 +26,7 @@ class OnroadTimerStatus(Enum):
class UIStateSP:
def __init__(self):
self.CP_SP: custom.CarParamsSP | None = None
self.params = Params()
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
@@ -38,6 +39,9 @@ class UIStateSP:
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.reset_onroad_sleep_timer()
self.CP_SP: custom.CarParamsSP | None = None
self.has_icbm: bool = False
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
def update(self) -> None:
if self.sunnylink_enabled:
@@ -120,6 +124,7 @@ class UIStateSP:
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo")
@@ -137,15 +142,14 @@ class UIStateSP:
self.torque_bar = self.params.get_bool("TorqueBar")
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
self.turn_signals = self.params.get_bool("ShowTurnSignals")
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
class DeviceSP:
def __init__(self):
self._params = Params()
def _set_awake(self, on: bool):
if on and self._params.get("DeviceBootMode", return_default=True) == 1:
self._params.put_bool("OffroadMode", True)
@staticmethod
def _set_awake(on: bool, _ui_state):
if _ui_state.boot_offroad_mode == 1 and not on:
_ui_state.params.put_bool("OffroadMode", True)
@staticmethod
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
+4 -15
View File
@@ -3,7 +3,6 @@ import os
import sys
import subprocess
import tempfile
import base64
import webbrowser
import argparse
from pathlib import Path
@@ -25,12 +24,6 @@ def compare_frames(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...")
@@ -60,20 +53,16 @@ def find_differences(video1, video2):
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)
return different_frames, len(frames1)
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
def generate_html_report(video1, video2, basedir, different_frames, total_frames):
chunks = []
if different_frames:
current_chunk = [different_frames[0]]
@@ -177,14 +166,14 @@ def main():
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)
different_frames, 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)
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames)
with open(DIFF_OUT_DIR / args.output, 'w') as f:
f.write(html)
File diff suppressed because it is too large Load Diff
-1
View File
@@ -6,7 +6,6 @@
"Español": "es",
"Türkçe": "tr",
"Українська": "uk",
"العربية": "ar",
"ไทย": "th",
"中文(繁體)": "zh-CHT",
"中文(简体)": "zh-CHS",
+1 -1
View File
@@ -299,7 +299,7 @@ class Device(DeviceSP):
def _set_awake(self, on: bool):
if on != self._awake:
DeviceSP._set_awake(self, on)
DeviceSP._set_awake(on, ui_state)
self._awake = on
cloudlog.debug(f"setting display power {int(on)}")
HARDWARE.set_display_power(on)
@@ -17,13 +17,16 @@ class BlinkerPauseLateral:
self.enabled = self.params.get_bool("BlinkerPauseLateralControl")
self.is_metric = self.params.get_bool("IsMetric")
self.min_speed = 0
self.reengage_delay = 0
self.blinker_off_timer = 0.0
def get_params(self) -> None:
self.enabled = self.params.get_bool("BlinkerPauseLateralControl")
self.is_metric = self.params.get_bool("IsMetric")
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed")
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True)
self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True)
def update(self, CS: car.CarState) -> bool:
def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool:
if not self.enabled:
return False
@@ -31,4 +34,11 @@ class BlinkerPauseLateral:
speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
min_speed_ms = self.min_speed * speed_factor
return bool(one_blinker and CS.vEgo < min_speed_ms)
below_speed = CS.vEgo < min_speed_ms
if one_blinker and below_speed:
self.blinker_off_timer = self.reengage_delay
elif self.blinker_off_timer > 0:
self.blinker_off_timer -= DT_CTRL
return bool((one_blinker and below_speed) or self.blinker_off_timer > 0)
@@ -5,6 +5,7 @@ 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 numpy as np
import pytest
import cereal.messaging as messaging
from cereal import custom, log
@@ -12,11 +13,52 @@ from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
def _th_above_f32(th: float) -> float:
"""
Return the next representable float32 *above* `th`.
This avoids flaky comparisons around thresholds due to float32 rounding.
"""
th32 = np.float32(th)
above32 = np.nextafter(th32, np.float32(np.inf), dtype=np.float32)
return float(above32)
def _build_single_spike_filtered(n: int, base: float = 1.0) -> np.ndarray:
"""
Create an array where max() is >= threshold but p97 is < threshold.
This demonstrates the behavior difference vs np.amax().
Note: We intentionally construct using float32-representable values to match
the data path through cereal/capnp.
"""
th = float(_ENTERING_PRED_LAT_ACC_TH)
th32 = float(np.float32(th))
# numpy percentile default is linear interpolation: idx=(n-1)*p/100
idx = (n - 1) * 0.97
w = float(idx - np.floor(idx))
base32 = float(np.float32(base))
# Choose spike so that p97 = base + w*(spike-base) < th
# -> spike < base + (th-base)/w. Use a margin (0.9) and ensure spike >= th.
if w == 0.0:
spike = th32 + 1.0
else:
spike = base32 + (th32 - base32) / w * 0.9
spike = max(spike, th32 + 0.01)
arr = np.full(n, base32, dtype=np.float32)
arr[-1] = np.float32(spike)
return arr
def generate_modelV2():
model = messaging.new_message('modelV2')
position = log.XYZTData.new_message()
@@ -101,4 +143,72 @@ class TestSmartCruiseControlVision:
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
assert self.scc_v.state == VisionState.enabled
@pytest.mark.parametrize(
"case, should_enter",
[
("p97_just_above_threshold", True),
("single_spike_filtered", False),
("persistent_high_values", True),
],
ids=[
"p97>threshold_enters",
"single_spike_max_large_but_p97_below_threshold",
"high_values_persist_trigger_entering",
],
)
def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter):
n = len(ModelConstants.T_IDXS)
th = float(_ENTERING_PRED_LAT_ACC_TH)
if case == "p97_just_above_threshold":
# Use the next representable float32 above threshold to avoid float32 rounding flakiness.
val = _th_above_f32(th)
pred_lat_accels = np.full(n, np.float32(val), dtype=np.float32)
elif case == "single_spike_filtered":
pred_lat_accels = _build_single_spike_filtered(n, base=1.0)
elif case == "persistent_high_values":
# Make enough "high" samples so p97 is driven by the persistent trend, not a single outlier.
high_count = max(2, int(np.ceil(n * 0.03)) + 1)
pred_lat_accels = np.full(n, np.float32(1.0), dtype=np.float32)
pred_lat_accels[-high_count:] = np.float32(2.0)
pred_lat_accels[-1] = np.float32(8.0) # keep one big outlier too
else:
raise AssertionError(f"Unknown case: {case}")
# Override model predictions so:
# predicted_lat_accels = abs(orientationRate.z) * velocity.x == pred_lat_accels
mdl = generate_modelV2()
mdl.modelV2.velocity.x = [1.0 for _ in range(n)]
mdl.modelV2.orientationRate.z = [float(x) for x in pred_lat_accels]
self.sm["modelV2"] = mdl.modelV2
v_ego = float(MIN_V + 5.0)
# 1st update: disabled -> enabled
self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0)
# 2nd update: evaluate entering condition from enabled state
self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0)
# Controller does percentile on numpy float64 arrays (values already quantized by capnp),
# so compute expected in float64 to match behavior and avoid interpolation/rounding deltas.
expected_p97 = float(np.percentile(pred_lat_accels.astype(np.float64), 97))
# allow tiny numeric differences due to float conversions/interpolation
assert np.isclose(self.scc_v.max_pred_lat_acc, expected_p97, rtol=1e-6, atol=1e-5)
if should_enter:
# We assert entering primarily by state (this is the actual intended behavior).
assert self.scc_v.state == VisionState.entering
# Optional sanity: should be >= threshold with some margin (since we used nextafter above threshold).
assert self.scc_v.max_pred_lat_acc > th
else:
# Difference vs np.amax(): max can be above threshold, but p97 stays below it.
assert float(np.max(pred_lat_accels)) >= th
assert self.scc_v.max_pred_lat_acc < th
assert self.scc_v.state == VisionState.enabled
# TODO-SP: mock modelV2 data to test other states
@@ -90,7 +90,7 @@ class SmartCruiseControlVision:
# get the maximum lat accel from the model
predicted_lat_accels = rate_plan * vel_plan
self.max_pred_lat_acc = np.amax(predicted_lat_accels)
self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97)
# get the maximum curve based on the current velocity
v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations
@@ -20,6 +20,8 @@ class TestBlinkerPauseLateral:
self.blinker_pause_lateral.enabled = True
self.blinker_pause_lateral.is_metric = False
self.blinker_pause_lateral.min_speed = 20 # MPH
self.blinker_pause_lateral.reengage_delay = 0
self.blinker_pause_lateral.blinker_off_timer = 0.0
self.CS = car.CarState.new_message()
self.CS.vEgo = 0
@@ -46,6 +48,18 @@ class TestBlinkerPauseLateral:
}
self._test_should_blinker_pause_lateral(expected_results)
def test_reengage_delay(self):
self.blinker_pause_lateral.reengage_delay = 2 # seconds
self.CS.vEgo = 4.5 # ~10 MPH
expected_results = {
(False, False): True,
(True, False): True,
(False, True): True,
(True, True): False
}
self._test_should_blinker_pause_lateral(expected_results)
def test_above_min_speed_blinker(self):
self.CS.vEgo = 13.4 # ~30 MPH
@@ -93,6 +93,10 @@
"title": "[TIZI/TICI only] Blind Spot Detection",
"description": "Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported."
},
"BlinkerLateralReengageDelay": {
"title": "Post-Blinker Delay",
"description": "Delay before lateral control resumes after the turn signal ends."
},
"BlinkerMinLateralControlSpeed": {
"title": "Blinker Min Lateral Control Speed",
"description": ""
+1
View File
@@ -166,6 +166,7 @@ class SunnylinkState:
def _worker_thread(self) -> None:
while self._running:
self._sm.update()
if self.is_connected():
self._fetch_roles()
self._fetch_users()
@@ -3,7 +3,7 @@ import pytest
import time
import numpy as np
from dataclasses import dataclass
from tabulate import tabulate
from openpilot.common.utils import tabulate
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
+59
View File
@@ -115,6 +115,55 @@ def _parse_proc_stat(stat: str) -> ProcStat | None:
cloudlog.exception("failed to parse /proc/<pid>/stat")
return None
class SmapsData(TypedDict):
pss: int # bytes
pss_anon: int # bytes
pss_shmem: int # bytes
_SMAPS_KEYS = {b'Pss:', b'Pss_Anon:', b'Pss_Shmem:'}
# smaps_rollup (kernel 4.14+) is ideal but missing on some BSP kernels;
# fall back to per-VMA smaps (any kernel). Pss_Anon/Pss_Shmem only in 5.x+.
_smaps_path: str | None = None # auto-detected on first call
# per-VMA smaps is expensive (kernel walks page tables for every VMA).
# cache results and only refresh every N cycles to keep CPU low.
_smaps_cache: dict[int, SmapsData] = {}
_smaps_cycle = 0
_SMAPS_EVERY = 20 # refresh every 20th cycle (40s at 0.5Hz)
def _read_smaps(pid: int) -> SmapsData:
global _smaps_path
try:
if _smaps_path is None:
_smaps_path = 'smaps_rollup' if os.path.exists(f'/proc/{pid}/smaps_rollup') else 'smaps'
result: SmapsData = {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0}
with open(f'/proc/{pid}/{_smaps_path}', 'rb') as f:
for line in f:
parts = line.split()
if len(parts) >= 2 and parts[0] in _SMAPS_KEYS:
val = int(parts[1]) * 1024 # kB -> bytes
if parts[0] == b'Pss:':
result['pss'] += val
elif parts[0] == b'Pss_Anon:':
result['pss_anon'] += val
elif parts[0] == b'Pss_Shmem:':
result['pss_shmem'] += val
return result
except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
return {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0}
def _get_smaps_cached(pid: int) -> SmapsData:
"""Return cached smaps data, refreshing every _SMAPS_EVERY cycles."""
if _smaps_cycle == 0 or pid not in _smaps_cache:
_smaps_cache[pid] = _read_smaps(pid)
return _smaps_cache.get(pid, {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0})
class ProcExtra(TypedDict):
pid: int
name: str
@@ -189,6 +238,13 @@ def build_proc_log_message(msg) -> None:
for j, arg in enumerate(extra['cmdline']):
cmdline[j] = arg
# smaps is expensive (kernel walks page tables); skip small processes, use cache
if r['rss'] * PAGE_SIZE > 5 * 1024 * 1024:
smaps = _get_smaps_cached(r['pid'])
proc.memPss = smaps['pss']
proc.memPssAnon = smaps['pss_anon']
proc.memPssShmem = smaps['pss_shmem']
cpu_times = _cpu_times()
cpu_list = pl.init('cpuTimes', len(cpu_times))
for i, ct in enumerate(cpu_times):
@@ -212,6 +268,9 @@ def build_proc_log_message(msg) -> None:
pl.mem.inactive = mem_info["Inactive:"]
pl.mem.shared = mem_info["Shmem:"]
global _smaps_cycle
_smaps_cycle = (_smaps_cycle + 1) % _SMAPS_EVERY
def main() -> NoReturn:
pm = messaging.PubMaster(['procLog'])
-1
View File
@@ -16,7 +16,6 @@ TRANSLATIONS_DIR = UI_DIR.joinpath("translations")
LANGUAGES_FILE = TRANSLATIONS_DIR.joinpath("languages.json")
UNIFONT_LANGUAGES = [
"ar",
"th",
"zh-CHT",
"zh-CHS",
+8 -2
View File
@@ -73,8 +73,14 @@ class GuiScrollPanel2:
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)
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
if self._state == ScrollState.STEADY:
# if we find ourselves out of bounds, scroll back in (from external layout dimension changes, etc.)
if self.get_offset() > max_offset or self.get_offset() < min_offset:
self._state = ScrollState.AUTO_SCROLL
elif self._state == ScrollState.AUTO_SCROLL:
# simple exponential return if out of bounds
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
if out_of_bounds and self._handle_out_of_bounds:
+1 -1
View File
@@ -631,7 +631,7 @@ class WifiManager:
known_connections = self._get_connections()
networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()]
# sort with quantized strength to reduce jumping
networks.sort(key=lambda n: (-n.is_connected, -round(n.strength / 100 * 2), n.ssid.lower()))
networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower()))
self._networks = networks
self._update_ipv4_address()
+6 -12
View File
@@ -94,12 +94,12 @@ class NetworkConnectivityMonitor:
class SetupState(IntEnum):
GETTING_STARTED = 0
NETWORK_SETUP = 1
NETWORK_SETUP_CUSTOM_SOFTWARE = 8
SOFTWARE_SELECTION = 2
CUSTOM_SOFTWARE = 3
DOWNLOADING = 4
DOWNLOAD_FAILED = 5
CUSTOM_SOFTWARE_WARNING = 6
NETWORK_SETUP_CUSTOM_SOFTWARE = 2
SOFTWARE_SELECTION = 3
CUSTOM_SOFTWARE = 4
DOWNLOADING = 5
DOWNLOAD_FAILED = 6
CUSTOM_SOFTWARE_WARNING = 7
class StartPage(Widget):
@@ -590,15 +590,9 @@ class Setup(Widget):
def _custom_software_warning_back_button_callback(self):
self._set_state(SetupState.SOFTWARE_SELECTION)
def _custom_software_warning_continue_button_callback(self):
self._set_state(SetupState.CUSTOM_SOFTWARE)
def _getting_started_button_callback(self):
self._set_state(SetupState.SOFTWARE_SELECTION)
def _software_selection_back_button_callback(self):
self._set_state(SetupState.GETTING_STARTED)
def _software_selection_continue_button_callback(self):
self.use_openpilot()
+18
View File
@@ -0,0 +1,18 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
def get_highlighted_description(params, param_name: str, descriptions: list[str]) -> str:
index = int(params.get(param_name, return_default=True))
lines = []
for i, desc in enumerate(descriptions):
if i == index:
lines.append(f"<b>{desc}</b>")
else:
lines.append(f"{desc}")
return "<br>".join(lines)
+33 -2
View File
@@ -134,6 +134,10 @@ class MultipleButtonActionSP(MultipleButtonAction):
if self.param_key:
self.selected_button = int(self.params.get(self.param_key, return_default=True))
self._anim_x: float | None = None
self.enabled_buttons: set[int] | None = None
def set_enabled_buttons(self, indices: set[int] | None):
self.enabled_buttons = indices
def _render(self, rect: rl.Rectangle):
@@ -171,10 +175,31 @@ class MultipleButtonActionSP(MultipleButtonAction):
text_x = button_x + (self.button_width - text_size.x) / 2
text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
# Check individual button enabled state
is_button_enabled = self.enabled and (self.enabled_buttons is None or i in self.enabled_buttons)
current_text_color = text_color if is_button_enabled else style.MBC_DISABLED
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, current_text_color)
def _handle_mouse_release(self, mouse_pos: MousePos):
MultipleButtonAction._handle_mouse_release(self, mouse_pos)
# Override parent method to check individual button enabled state
if not self.enabled:
return
button_y = self._rect.y + (self._rect.height - style.BUTTON_HEIGHT) / 2
for i, _ in enumerate(self.buttons):
button_x = self._rect.x + i * self.button_width
button_rect = rl.Rectangle(button_x, button_y, self.button_width, style.BUTTON_HEIGHT)
if rl.check_collision_point_rec(mouse_pos, button_rect):
# Check if this specific button is enabled
if self.enabled_buttons is not None and i not in self.enabled_buttons:
return
self.selected_button = i
if self.callback:
self.callback(i)
if self.param_key:
self.params.put(self.param_key, self.selected_button)
@@ -212,6 +237,12 @@ class ListItemSP(ListItem):
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
self._rect.height = self.get_item_height(self._font, content_width)
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
super().set_parent_rect(parent_rect)
if self.description_visible:
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
self._rect.height = self.get_item_height(self._font, content_width)
def get_item_height(self, font: rl.Font, max_width: int) -> float:
height = super().get_item_height(font, max_width)
+12 -4
View File
@@ -195,7 +195,7 @@ NAV_BAR_WIDTH = 205
NAV_BAR_HEIGHT = 8
DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing
DISMISS_TIME_SECONDS = 1.5
DISMISS_TIME_SECONDS = 2.0
class NavBar(Widget):
@@ -242,6 +242,7 @@ class NavWidget(Widget, abc.ABC):
self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1)
self._playing_dismiss_animation = False
self._trigger_animate_in = False
self._nav_bar_show_time = 0.0
self._back_enabled: bool | Callable[[], bool] = True
self._nav_bar = NavBar()
@@ -330,6 +331,7 @@ class NavWidget(Widget, abc.ABC):
if self._trigger_animate_in:
self._pos_filter.x = self._rect.height
self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT
self._nav_bar_show_time = rl.get_time()
self._trigger_animate_in = False
new_y = 0.0
@@ -366,18 +368,24 @@ class NavWidget(Widget, abc.ABC):
if self.back_enabled:
bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2
nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4
# User dragging or dismissing, nav bar follows NavWidget
if self._back_button_start_pos is not None or self._playing_dismiss_animation:
self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._pos_filter.x
# Waiting to show
elif nav_bar_delayed:
self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT
# Animate back to top
else:
self._nav_bar_y_filter.update(NAV_BAR_MARGIN)
self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x))
self._nav_bar.render()
# draw black above widget when dismissing
if self._rect.y > 0:
rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK)
self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x))
self._nav_bar.render()
return ret
def show_event(self):
+51 -10
View File
@@ -15,7 +15,6 @@ ANIMATION_SCALE = 0.6
MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds
DO_ZOOM = False
DO_JELLO = False
SCROLL_BAR = False
class LineSeparator(Widget):
@@ -33,9 +32,52 @@ class LineSeparator(Widget):
LINE_COLOR)
class ScrollIndicator(Widget):
HORIZONTAL_MARGIN = 4
def __init__(self):
super().__init__()
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48)
self._scroll_offset: float = 0.0
self._content_size: float = 0.0
self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None:
self._scroll_offset = scroll_offset
self._content_size = content_size
self._viewport = viewport
def _render(self, _):
# scale indicator width based on content size
indicator_w = float(np.interp(self._content_size, [1000, 3000], [300, 100]))
# position based on scroll ratio
slide_range = self._viewport.width - indicator_w
max_scroll = self._content_size - self._viewport.width
scroll_ratio = -self._scroll_offset / max_scroll
x = self._viewport.x + scroll_ratio * slide_range
# don't bounce up when NavWidget shows
y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2
# squeeze when overscrolling past edges
dest_left = max(x, self._viewport.x)
dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width)
dest_w = max(indicator_w / 2, dest_right - dest_left)
# keep within viewport after applying minimum width
dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w)
dest_left = max(dest_left, self._viewport.x)
src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height)
dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height)
rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0,
rl.Color(255, 255, 255, int(255 * 0.45)))
class Scroller(Widget):
def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING,
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING):
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING,
scroll_indicator: bool = True):
super().__init__()
self._items: list[Widget] = []
self._horizontal = horizontal
@@ -65,7 +107,8 @@ class Scroller(Widget):
self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
self._scroll_enabled: bool | Callable[[], bool] = True
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80)
self._show_scroll_indicator = scroll_indicator
self._scroll_indicator = ScrollIndicator()
for item in items:
self.add_widget(item)
@@ -241,15 +284,13 @@ class Scroller(Widget):
else:
item.render()
# Draw scroll indicator
if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0:
_real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height
scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height
scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height)
rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE)
rl.end_scissor_mode()
# Draw scroll indicator
if self._show_scroll_indicator and self._horizontal and len(self._visible_items) > 0:
self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect)
self._scroll_indicator.render()
def show_event(self):
super().show_event()
if self._reset_scroll_at_show:
+5 -1
View File
@@ -1,5 +1,9 @@
acados_repo/
lib
/lib
!x86_64/
!larch64/
!aarch64/
!Darwin/
!*.so
!*.so.*
!*.dylib
+1
View File
@@ -0,0 +1 @@
+3
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
set -e
export SOURCE_DATE_EPOCH=0
export ZERO_AR_DATE=1
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
ARCHNAME="x86_64"
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:821ce18f417d211c4845b60482d465b809f90dc7d04f023d652d8221e87679b1
size 553544
oid sha256:05a1ba3cf37fa929cdd56f892608b2f89c35a05ef1b07fedb86b2f0d76607263
size 540488
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3feea7927d004064bbc5a13c3287467669ce801cb0a3c616cf9e089816da5a0b
size 2155088
oid sha256:c0bf22898d9c59b672d3d0961f5f4c804b9957478125d99eb297de3091bedd15
size 2416112
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a042716f515913786581dff39799eb71fc66caddfa18b1c9f0d54f00c1568fd2
size 1572648
oid sha256:5b6875fb47940764d4ebb916c2373cb0e04929229feb654b290676c28d48fa9d
size 1531024
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6abea4815e3f03cff06fe8a9602e97f9acf102f18f803571460a94595b93be4
size 262824
oid sha256:04be908c3f707e5c968022b9cdd79ab75ae7af46e7fa019ceee98f854ddd3f64
size 262464
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a360d4b53826b91ada3358156d44a14d497bdd8ace88707fd4b386ed6d194c7
size 17503920
oid sha256:a53ae46650c4df5b0ddb87a658f59a0422e41743e8bc2d822da0aefd1d280791
size 5088536
Vendored Executable
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
# Reproducible builds: pin timestamps to epoch
export SOURCE_DATE_EPOCH=0
export ZERO_AR_DATE=1
pids=()
names=()
logs=()
for script in "$DIR"/*/build.sh; do
[ -f "$script" ] || continue
name=$(basename "$(dirname "$script")")
log=$(mktemp)
names+=("$name")
logs+=("$log")
(cd "$(dirname "$script")" && bash "$(basename "$script")") >"$log" 2>&1 &
pids+=($!)
done
failed=0
for i in "${!pids[@]}"; do
echo "--- ${names[$i]} ---"
if wait "${pids[$i]}"; then
echo "OK"
else
echo "FAILED (exit $?)"
failed=1
fi
cat "${logs[$i]}"
rm -f "${logs[$i]}"
echo
done
[ $failed -ne 0 ] && exit $failed
# Repack ar archives with deterministic headers (zero timestamps/uid/gid)
# Skip foreign-platform archives that ar can't read (e.g. Mach-O on Linux)
while IFS= read -r -d '' lib; do
tmpdir=$(mktemp -d)
lib=$(realpath "$lib")
if (cd "$tmpdir" && ar x "$lib" 2>/dev/null); then
(cd "$tmpdir" && ar Drcs repacked.a * && mv repacked.a "$lib")
fi
rm -rf "$tmpdir"
done < <(find "$DIR" -name '*.a' \
\( -path '*/x86_64/*' -o -path '*/Darwin/*' -o -path '*/larch64/*' -o -path '*/aarch64/*' \) \
-print0)
echo -e "\033[32mAll third_party builds succeeded.\033[0m"
+2 -1
View File
@@ -1 +1,2 @@
libyuv/
/libyuv/
!*.a
+3
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
set -e
export SOURCE_DATE_EPOCH=0
export ZERO_AR_DATE=1
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
ARCHNAME=$(uname -m)
+1 -1
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:320bef5a75a62dd2731a496040921d5000f1ed237ae70fd7aeb6c010a1534363
oid sha256:adafce26582e425164df7af36253ce58e3ed1dba9965650745c93bd96e42e976
size 462482
-1
View File
@@ -1 +0,0 @@
../include
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e21a3bd8df01cf4ce5461e7bf6654239196036c3f829255145265c7bf31a791d
size 511974
oid sha256:00f9759c67c6fa21657fabde9e096478ea5809716989599f673f638f039431e5
size 504790
+1
View File
@@ -1,3 +1,4 @@
/raylib_repo/
/raylib_python_repo/
/wheel/
!*.a
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ffe1fc6497f0c111fc507988e94fd29ce4db53a4876dc82ab9267895ad82584
size 6515352
oid sha256:fd045c1d4bca5c9b2ad044ea730826ff6cedeef0b64451b123717b136f1cd702
size 6392532

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