Compare commits

..

102 Commits

Author SHA1 Message Date
discountchubbs
ea4e165020 mas 2026-02-24 07:42:51 -08:00
discountchubbs
80663f0406 desired_accel 2026-02-24 07:39:05 -08:00
discountchubbs
6a741b0539 test 2026-02-21 19:38:08 -08:00
discountchubbs
66cc67cfcb mas 2026-02-21 19:32:16 -08:00
discountchubbs
f9695484ef whoops 2026-02-21 19:18:00 -08:00
discountchubbs
1f8f980729 Hyundai Longitudinal: dynamic -> minimal long tune 2026-02-21 19:15:43 -08:00
Christopher Haucke
082cf39d73 UI: Fix option control display for floating point params (#1711) 2026-02-21 20:01:27 -05:00
Jason Wen
5ccabb9d54 [TIZI/TICI] ui: use vCruiseCluster and vEgoCluster for SLA preActive (#1708) 2026-02-19 01:36:08 -05:00
github-actions[bot]
0bb2f8c9d4 [bot] Update Python packages (#1707)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-19 01:22:00 -05:00
Jason Wen
3a74f8c568 [TIZI/TICI] ui: ensure null checks for CarParams and CarParamsSP (#1706)
* [TIZI/TICI] ui: ensure null checks for `CarParams` and `CarParamsSP`

* space
2026-02-18 02:57:25 -05:00
Jason Wen
5eed9490c6 ci: remove double prebuilt workflow runs 2026-02-18 02:02:34 -05:00
github-actions[bot]
57b461a186 [bot] Update Python packages (#1704)
* Update Python packages

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-18 01:32:49 -05:00
Jason Wen
80e23509ba Update CHANGELOG.md 2026-02-17 22:33:28 -05:00
Jason Wen
c07ddcefb0 version: bump to 2026.001.000 2026-02-17 19:48:37 -05:00
Jason Wen
e8f65bc652 Controls: Support for Torque Lateral Control v0 Tune (#1693)
* init

* no

* more

* tree it

* final

* comment

* only with enforce torque

* only with enforce torque

* missed

* no

* lint

* Apply suggestion from @sunnyhaibin

* sunnylink metadata sync

* Apply suggestion from @sunnyhaibin

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
2026-02-17 13:17:03 -05:00
github-actions[bot]
f81e7245fe [bot] Update Python packages (#1702)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-16 04:18:44 -05:00
Jason Wen
0cb6b7b807 ci: exclude non-sunnypilot maintained submodules from repo maintenance (#1703)
* ci: exclude non-sunnypilot maintained submodules from repo maintenance

* match names
2026-02-16 04:16:49 -05:00
Jason Wen
4a869778a9 Revert "ci: skip uv lock upgrade on forks" (#1701)
Revert "ci: skip uv lock upgrade on forks (#1213)"

This reverts commit 220cfff04d.
2026-02-16 04:07:07 -05:00
Jason Wen
a120053d12 docker: Dockerfile.sunnypilot uv run scons (#1700)
* docker: Dockerfile.sunnypilot uv run scons

* docker: Dockerfile.sunnypilot: don't set UV_PROJECT_ENVIRONMENT
2026-02-16 03:32:08 -05:00
James Vecellio-Grant
a48988ccb3 chore: sync tinygrad (#1680)
* chore: sync tinygrad

Runs great in sim. now we need to rebuild some models

* oops forgot to unblock this after testing

* helpers

* oh yeah

* latest tg

* this wont do anything empriically

* reduce complexity

* okay lint

* Update tinygrad_runner.py

* Update modeld.py

* Update build-all-tinygrad-models.yaml

* tinygrad bump

* Update modeld.py

* Update tinygrad_runner.py

* bump

* Update SConscript

* Update SConscript

* com

* Update fetcher.py

* Update helpers.py

* Update model_runner.py

* Update model_runner.py

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-16 03:01:59 -05:00
Jason Wen
0650964559 [TIZI/TICI] ui: only fetch roles and users when the sunnylink panel is opened (#1697)
* sunnylink: only roles and users when the sunnylink panel is opened

* shadow

* thread-safe
2026-02-13 23:39:55 -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
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
161 changed files with 2925 additions and 3748 deletions

View File

@@ -140,7 +140,7 @@ jobs:
run: |
echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json
built=(); while IFS= read -r line; do built+=("$line"); done < <(
ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
)
jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \
'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json
@@ -168,6 +168,7 @@ jobs:
if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
model: ${{ fromJson(needs.setup.outputs.model_matrix) }}

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

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"
@@ -43,14 +43,22 @@ jobs:
with:
submodules: true
- name: uv lock
if: github.repository == 'commaai/openpilot'
run: |
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 '*'
git config submodule.msgq.update none
git config submodule.rednose_repo.update none
git config submodule.teleoprtc_repo.update none
git config submodule.tinygrad.update none
git submodule update --remote
git add .
@@ -61,7 +69,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 +78,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

View File

@@ -241,10 +241,3 @@ jobs:
gh run watch "$RUN_ID"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger prebuilt workflow
if: success() && steps.push-changes.outputs.has_changes == 'true'
run: |
gh workflow run sunnypilot-build-prebuilt.yaml --ref "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

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
.gitignore vendored
View File

@@ -100,6 +100,7 @@ Pipfile
.ionide
.claude/
.context/
PLAN.md
TASK.md

2
.gitmodules vendored
View File

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

View File

@@ -1,5 +1,104 @@
sunnypilot Version 2025.003.000 (20xx-xx-xx)
sunnypilot Version 2026.001.000 (2026-03-xx)
========================
* What's Changed (sunnypilot/sunnypilot)
* Complete rewrite of the user interface from Qt C++ to Raylib Python
* comma four support
* ui: sunnypilot toggle style by @nayan8teen
* ui: fix scroll panel mouse wheel behavior by @nayan8teen
* ui: sunnypilot panels by @nayan8teen
* sunnylink: centralize key pair handling in sunnylink registration by @devtekve
* ui: reimplement sunnypilot branding with Raylib by @sunnyhaibin
* ui: Platform Selector by @Discountchubbs
* ui: vehicle brand settings by @Discountchubbs
* ui: sunnylink client-side implementation by @nayan8teen
* ui: `NetworkUISP` by @Discountchubbs
* ui: add sunnypilot font by @nayan8teen
* ui: sunnypilot sponsor tier color mapping by @sunnyhaibin
* ui: sunnylink panel by @nayan8teen
* ui: Models panel by @Discountchubbs
* ui: software panel by @Discountchubbs
* modeld_v2: support planplus outputs by @Discountchubbs
* ui: OSM panel by @Discountchubbs
* ui: Developer panel extension by @Discountchubbs
* sunnylink: Vehicle Selector support by @sunnyhaibin
* [TIZI/TICI] ui: Developer Metrics by @rav4kumar
* [comma 4] ui: sunnylink panel by @nayan8teen
* ui: lateral-only and longitudinal-only UI statuses support by @royjr
* sunnylink: elliptic curve keys support and improve key path handling by @nayan8teen
* sunnylink: block remote modification of SSH key parameters by @zikeji
* [TIZI/TICI] ui: rainbow path by @rav4kumar
* [TIZI/TICI] ui: chevron metrics by @rav4kumar
* ui: include MADS enabled state to `engaged` check by @sunnyhaibin
* Toyota: Enforce Factory Longitudinal Control by @sunnyhaibin
* ui: fix malformed dongle ID display on the PC if dongleID is not set by @dzid26
* SL: Re enable and validate ingestion of swaglogs by @devtekve
* modeld_v2: planplus model tuning by @Discountchubbs
* ui: fix Always Offroad button visibility by @nayan8teen
* Reimplement sunnypilot Terms of Service & sunnylink Consent Screens by @sunnyhaibin
* [TIZI/TICI] ui: update dmoji position and Developer UI adjustments by @rav4kumar
* modeld: configurable camera offset by @Discountchubbs
* [TIZI/TICI] ui: sunnylink status on sidebar by @Copilot
* ui: Global Brightness Override by @nayan8teen
* ui: Customizable Interactive Timeout by @sunnyhaibin
* sunnylink: add units to param metadata by @nayan8teen
* ui: Customizable Onroad Brightness by @sunnyhaibin
* [TIZI/TICI] ui: Steering panel by @nayan8teen
* [TIZI/TICI] ui: Rocket Fuel by @rav4kumar
* [TIZI/TICI] ui: MICI style turn signals by @rav4kumar
* [TIZI/TICI] ui: MICI style blindspot indicators by @sunnyhaibin
* [MICI] ui: display blindspot indicators when available by @rav4kumar
* [TIZI/TICI] ui: Road Name by @rav4kumar
* [TIZI/TICI] ui: Blue "Exit Always Offroad" button by @dzid26
* [TIZI/TICI] ui: Speed Limit by @rav4kumar
* Reapply "latcontrol_torque: lower kp and lower friction threshold (commaai/openpilot#36619)" by @sunnyhaibin
* [TIZI/TICI] ui: steering arc by @royjr
* [TIZI/TICI] ui: Smart Cruise Control elements by @sunnyhaibin
* [TIZI/TICI] ui: Green Light and Lead Departure elements by @sunnyhaibin
* [TIZI/TICI] ui: standstill timer by @sunnyhaibin
* [MICI] ui: driving models selector by @Discountchubbs
* [TIZI/TICI] ui: Hide vEgo and True vEgo by @sunnyhaibin
* [TIZI/TICI] ui: Visuals panel by @nayan8teen
* Device: Retain QuickBoot state after op switch by @nayan8teen
* [TIZI/TICI] ui: Trips panel by @sunnyhaibin
* [TIZI/TICI] ui: dynamic ICBM status by @sunnyhaibin
* [TIZI/TICI] ui: Cruise panel by @sunnyhaibin
* ui: better wake mode support by @nayan8teen
* Pause Lateral Control with Blinker: Post-Blinker Delay by @CHaucke89
* SCC-V: Use p97 for predicted lateral accel by @yasu-oh
* Controls: Support for Torque Lateral Control v0 Tune by @sunnyhaibin
* What's Changed (sunnypilot/opendbc)
* Honda: DBC for Accord 9th Generation by @mvl-boston
* FCA: update tire stiffness values for `RAM_HD` by @dparring
* Honda: Nidec hybrid baseline brake support by @mvl-boston
* Subaru Global Gen2: bump steering limits and update tuning by @sunnyhaibin
* Toyota: Enforce Stock Longitudinal Control by @rav4kumar
* Nissan: use MADS enabled status for LKAS HUD logic by @downquark7
* Reapply "Lateral: lower friction threshold (#2915)" (#378) by @sunnyhaibin
* HKG: add KIA_FORTE_2019_NON_SCC fingerprint by @royjr
* Nissan: Parse cruise control buttons by @downquark7
* Rivian: Add stalk down ACC behavior to match stock Rivian by @lukasloetkolben
* Tesla: remove `TESLA_MODEL_X` from `dashcamOnly` by @ssysm
* Hyundai Longitudinal: refactor tuning by @Discountchubbs
* Tesla: add fingerprint for Model 3 Performance HW4 by @sunnyhaibin
* Toyota: do not disable radar when smartDSU or CAN Filter detected by @sunnyhaibin
* Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston
* GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin
* GM: remove `CHEVROLET_BOLT_NON_ACC_2ND_GEN` from `dashcamOnly` by @sunnyhaibin
* New Contributors (sunnypilot/sunnypilot)
* @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots"
* @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters"
* @Candy0707 made their first contribution in "[TIZI/TICI] ui: Fix misaligned turn signals and blindspot indicators with sidebar"
* @CHaucke89 made their first contribution in "Pause Lateral Control with Blinker: Post-Blinker Delay"
* @yasu-oh made their first contribution in "SCC-V: Use p97 for predicted lateral accel"
* New Contributors (sunnypilot/opendbc)
* @AmyJeanes made their first contribution in "Tesla: Fix stock LKAS being blocked when MADS is enabled"
* @mvl-boston made their first contribution in "Honda: Update Clarity brake to renamed DBC message name"
* @dzid26 made their first contribution in "Tesla: Parse speed limit from CAN"
* @firestar5683 made their first contribution in "GM: Non-ACC platforms with steering only support"
* @downquark7 made their first contribution in "Nissan: use MADS enabled status for LKAS HUD logic"
* @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint"
* @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`"
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000
sunnypilot Version 2025.002.000 (2025-11-06)
========================

View File

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

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

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

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

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

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

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

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

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"}},
@@ -229,8 +230,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
{"ModeldSunny", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AlpamayoDriveFast", {PERSISTENT | BACKUP, BOOL, "0"}},
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
@@ -269,6 +268,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT}},
{"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}},
{"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}},

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)

2
panda

Submodule panda updated: ed8a6f9ec2...a95e060e85

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 = [

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

Binary file not shown.

View File

@@ -64,6 +64,8 @@ class Controls(ControlsExt):
elif self.CP.lateralTuning.which() == 'torque':
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI, DT_CTRL)
self.LaC = ControlsExt.initialize_lateral_control(self, self.LaC, self.CI, DT_CTRL)
def update(self):
self.sm.update(15)
if self.sm.updated["liveCalibration"]:

View File

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

View File

@@ -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
selfdrive/debug/mem_usage.py Executable file
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)

View File

@@ -3,15 +3,14 @@ import time
from cereal import car, log, messaging
from openpilot.common.params import Params
from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model, is_modeld_sunny
from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model
from openpilot.system.hardware import HARDWARE
if __name__ == "__main__":
CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10)
params = Params()
params.put("CarParams", CP.to_bytes())
if is_modeld_sunny:= is_modeld_sunny(False, params, CP):
print("Using sunnypilot custom modeld")
if use_snpe_modeld := is_snpe_model(False, params, CP):
print("Using SNPE modeld")
if use_tinygrad_modeld := is_tinygrad_model(False, params, CP):

View File

@@ -270,7 +270,7 @@ def main():
estimator = LocationEstimator(DEBUG)
filter_initialized = False
critcal_services = ["accelerometer", "gyroscope"]
critcal_services = ["accelerometer", "gyroscope", "cameraOdometry"]
observation_input_invalid = defaultdict(int)
input_invalid_limit = {s: round(INPUT_INVALID_LIMIT * (SERVICE_LIST[s].frequency / 20.)) for s in critcal_services}
@@ -320,7 +320,8 @@ def main():
filter_initialized = sm.all_checks() and sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
if sm.updated["cameraOdometry"]:
inputs_valid = True
critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critcal_services)
inputs_valid = sm.all_valid() and critical_service_inputs_valid
sensors_valid = sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
msg = estimator.get_msg(sensors_valid, inputs_valid, filter_initialized)

View File

@@ -50,7 +50,7 @@ def tg_compile(flags, model_name):
# Compile small models
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
flags = {
'larch64': 'DEV=QCOM',
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
tg_compile(flags, model_name)

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

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}

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,

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

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

View File

@@ -91,7 +91,7 @@ class SelfdriveD(CruiseHelper):
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
if SIMULATION:
ignore += ['driverCameraState', 'managerState', 'livePose', 'liveCalibration', 'liveParameters', 'liveTorqueParameters', 'liveDelay', 'driverAssistance']
ignore += ['driverCameraState', 'managerState']
if REPLAY:
# no vipc in replay will make them ignored anyways
ignore += ['roadCameraState', 'wideRoadCameraState']
@@ -388,8 +388,8 @@ class SelfdriveD(CruiseHelper):
if not self.CP.notCar:
if not self.sm['livePose'].posenetOK:
self.events.add(EventName.posenetInvalid)
# if not self.sm['livePose'].inputsOK:
# self.events.add(EventName.locationdTemporaryError)
if not self.sm['livePose'].inputsOK:
self.events.add(EventName.locationdTemporaryError)
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
self.events.add(EventName.paramsdTemporaryError)

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

View File

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

View File

@@ -1 +0,0 @@
67f3daf309dc6cbb6844fcbaeb83e6596637e551

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

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

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

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

View File

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

View File

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

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

View File

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

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:

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)

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

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

View File

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

View File

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

View File

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

View File

@@ -99,7 +99,7 @@ class ModelsLayout(Widget):
"Keeping this on provides the stock openpilot experience.")
if lagd_toggle:
desc += f"<br>{tr('Live Steer Delay:')} {ui_state.sm['liveDelay'].lateralDelay:.3f} s"
elif ui_state.CP:
elif ui_state.CP is not None:
sw = float(ui_state.params.get("LagdToggleDelay", "0.2"))
cp = ui_state.CP.steerActuatorDelay
desc += f"<br>{tr('Actuator Delay:')} {cp:.2f} s + {tr('Software Delay:')} {sw:.2f} s = {tr('Total Delay:')} {cp + sw:.2f} s"

View File

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

View File

@@ -75,7 +75,7 @@ class LaneChangeSettingsLayout(Widget):
self._scroller.show_event()
def _update_toggles(self):
enable_bsm = ui_state.CP and ui_state.CP.enableBsm
enable_bsm = ui_state.CP is not None and ui_state.CP.enableBsm
if not enable_bsm and ui_state.params.get_bool("AutoLaneChangeBsmDelay"):
ui_state.params.remove("AutoLaneChangeBsmDelay")
self._bsm_delay.action_item.set_enabled(enable_bsm and ui_state.params.get("AutoLaneChangeTimer", return_default=True) > AutoLaneChangeMode.NUDGE)

View File

@@ -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
@@ -90,12 +91,12 @@ class MadsSettingsLayout(Widget):
if bundle:
brand = bundle.get("brand", "")
if not brand:
brand = ui_state.CP.brand if ui_state.CP else ""
brand = ui_state.CP.brand if ui_state.CP is not None else ""
if brand == "rivian":
return True
elif brand == "tesla":
return not (ui_state.CP_SP and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
return False
def _update_steering_mode_description(self, button_index: int):
@@ -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)

View File

@@ -4,15 +4,24 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import math
import os
from collections.abc import Callable
import pyray as rl
from openpilot.common.basedir import BASEDIR
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeFolder, TreeNode
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.network import NavButton
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
TORQUE_VERSIONS_PATH = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
class TorqueSettingsLayout(Widget):
@@ -20,10 +29,23 @@ class TorqueSettingsLayout(Widget):
super().__init__()
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
self._torque_version_dialog: TreeOptionDialog | None = None
self.cached_torque_versions = {}
self._load_versions()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _load_versions(self):
with open(TORQUE_VERSIONS_PATH) as f:
self.cached_torque_versions = json.load(f)
def _initialize_items(self):
self._torque_control_versions = ListItemSP(
title=tr("Torque Control Tune Version"),
description="Select the version of Torque Control Tune to use.",
action_item=NoElideButtonAction(tr("SELECT")),
callback=self._show_torque_version_dialog,
)
self._self_tune_toggle = toggle_item_sp(
param="LiveTorqueParamsToggle",
title=lambda: tr("Self-Tune"),
@@ -73,6 +95,7 @@ class TorqueSettingsLayout(Widget):
)
items = [
self._torque_control_versions,
self._self_tune_toggle,
self._relaxed_tune_toggle,
self._custom_tune_toggle,
@@ -103,6 +126,7 @@ class TorqueSettingsLayout(Widget):
title_text = tr("Real-Time & Offline") if ui_state.params.get("TorqueParamsOverrideEnabled") else tr("Offline Only")
self._torque_lat_accel_factor.set_title(lambda: tr("Lateral Acceleration Factor") + " (" + title_text + ")")
self._torque_friction.set_title(lambda: tr("Friction") + " (" + title_text + ")")
self._torque_control_versions.action_item.set_value(self._get_current_torque_version_label())
def _render(self, rect):
self._back_button.set_position(self._rect.x, self._rect.y + 20)
@@ -113,3 +137,54 @@ class TorqueSettingsLayout(Widget):
def show_event(self):
self._scroller.show_event()
def _get_current_torque_version_label(self):
current_val_bytes = ui_state.params.get("TorqueControlTune")
if current_val_bytes is None:
return tr("Default")
try:
current_val = float(current_val_bytes)
for label, info in self.cached_torque_versions.items():
if math.isclose(float(info["version"]), current_val, rel_tol=1e-5):
return label
except (ValueError, KeyError):
pass
return tr("Default")
def _show_torque_version_dialog(self):
options_map = {}
for label, info in self.cached_torque_versions.items():
try:
options_map[label] = float(info["version"])
except (ValueError, KeyError):
pass
# Sort options by label in descending order
sorted_labels = sorted(options_map.keys(), key=lambda k: options_map[k], reverse=True)
nodes = [TreeNode(tr("Default"))]
for label in sorted_labels:
nodes.append(TreeNode(label))
folders = [TreeFolder("", nodes)]
current_label = self._get_current_torque_version_label()
def handle_selection(result: int):
if result == DialogResult.CONFIRM and self._torque_version_dialog:
selected_ref = self._torque_version_dialog.selection_ref
if selected_ref == tr("Default"):
ui_state.params.remove("TorqueControlTune")
elif selected_ref in options_map:
ui_state.params.put("TorqueControlTune", options_map[selected_ref])
self._torque_version_dialog = None
self._torque_version_dialog = TreeOptionDialog(
tr("Select Torque Control Tune Version"),
folders,
current_ref=current_label,
option_font_weight=FontWeight.UNIFONT,
)
gui_app.set_modal_overlay(self._torque_version_dialog, callback=handle_selection)

View File

@@ -355,5 +355,10 @@ class SunnylinkLayout(Widget):
def show_event(self):
super().show_event()
ui_state.sunnylink_state.set_settings_open(True)
self._scroller.show_event()
self._sunnylink_description.set_visible(False)
def hide_event(self):
super().hide_event()
ui_state.sunnylink_state.set_settings_open(False)

View File

@@ -35,7 +35,7 @@ class VehicleLayout(Widget):
def get_brand():
if bundle := ui_state.params.get("CarPlatformBundle"):
return bundle.get("brand", "")
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
elif ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK":
return ui_state.CP.brand
return ""

View File

@@ -32,7 +32,7 @@ class HyundaiSettings(BrandSettings):
if bundle:
platform = bundle.get("platform")
self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
elif ui_state.CP:
elif ui_state.CP is not None:
self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable
tuning_param = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0")

View File

@@ -39,7 +39,7 @@ class SubaruSettings(BrandSettings):
platform = bundle.get("platform")
config = CAR[platform].config
self.has_stop_and_go = not (config.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
elif ui_state.CP:
elif ui_state.CP is not None:
self.has_stop_and_go = not (ui_state.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
disabled_msg = self.stop_and_go_disabled_msg()

View File

@@ -131,7 +131,7 @@ class PlatformSelector(Button):
self._platform = bundle.get("name", "")
self.set_text(self._platform)
self.color = style.BLUE
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
elif ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK":
self._platform = ui_state.CP.carFingerprint
self.set_text(self._platform)
self.color = style.GREEN

View File

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

View File

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

View File

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

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)

View File

@@ -22,6 +22,8 @@ from openpilot.system.ui.widgets import Widget
METER_TO_FOOT = 3.28084
METER_TO_MILE = 0.000621371
AHEAD_THRESHOLD = 5
SET_SPEED_NA = 255
KM_TO_MILE = 0.621371
AssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source
@@ -58,8 +60,11 @@ class SpeedLimitRenderer(Widget):
self.speed_limit_ahead_frame = 0
self.assist_frame = 0
self.speed = 0.0
self.set_speed = 0.0
self.is_cruise_set: bool = False
self.is_cruise_available: bool = True
self.set_speed: float = SET_SPEED_NA
self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False
self.font_bold = gui_app.font(FontWeight.BOLD)
self.font_demi = gui_app.font(FontWeight.SEMI_BOLD)
@@ -77,6 +82,8 @@ class SpeedLimitRenderer(Widget):
def update(self):
sm = ui_state.sm
if sm.recv_frame["carState"] < ui_state.started_frame:
self.set_speed = SET_SPEED_NA
self.speed = 0.0
return
if sm.updated["longitudinalPlanSP"]:
@@ -106,9 +113,21 @@ class SpeedLimitRenderer(Widget):
self.speed_limit_ahead_dist_prev = self.speed_limit_ahead_dist
cs = sm["carState"]
self.set_speed = cs.cruiseState.speed * self.speed_conv
v_ego = cs.vEgoCluster if cs.vEgoCluster != 0.0 else cs.vEgo
controls_state = sm['controlsState']
car_state = sm["carState"]
v_cruise_cluster = car_state.vCruiseCluster
self.set_speed = (
controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
)
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
self.is_cruise_available = self.set_speed != -1
if self.is_cruise_set and not ui_state.is_metric:
self.set_speed *= KM_TO_MILE
self.v_ego_cluster_seen = self.v_ego_cluster_seen or car_state.vEgoCluster != 0.0
v_ego = car_state.vEgoCluster if self.v_ego_cluster_seen else car_state.vEgo
self.speed = max(0.0, v_ego * self.speed_conv)
@staticmethod

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

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:

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

View File

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

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)

View File

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

View File

@@ -1 +1 @@
#define SUNNYPILOT_VERSION "2025.003.000"
#define SUNNYPILOT_VERSION "2026.001.000"

View File

@@ -1,32 +0,0 @@
import os
import glob
Import('env', 'arch')
lenv = env.Clone()
tinygrad_repo = env.Dir("#tinygrad_repo")
tinygrad_files = ["#"+x for x in glob.glob(tinygrad_repo.relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
def mayo_compile(flags, model_name):
pythonpath_string = f'PYTHONPATH="${{PYTHONPATH}}:{tinygrad_repo.abspath}"'
onnx_fn = f"distilled_models/{model_name}.onnx"
pkl_fn = f"distilled_models/{model_name}_tinygrad.pkl"
if not os.path.exists("distilled_models"):
try:
os.makedirs("distilled_models")
except OSError:
pass
if os.path.isfile(File(onnx_fn).abspath):
return lenv.Command(
pkl_fn,
[onnx_fn, "compile_split_tinygrad.py"] + tinygrad_files,
f'{pythonpath_string} {flags} python3 {File("compile_split_tinygrad.py").abspath} {File(onnx_fn).abspath} {File(pkl_fn).abspath}'
)
flags = {
'larch64': 'DEV=QCOM',
'Darwin': f'DEV=METAL HOME={os.path.expanduser("~")} IMAGE=0',
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
for m in ["student_vision", "student_policy"]:
mayo_compile(flags, m)

View File

@@ -1,109 +0,0 @@
"""
The whole point of this module is to mimic compile3.py while adapting it for our buffers to prevent buffer explosion
"""
import os
import sys
import pickle
from tinygrad import Tensor, TinyJit, Context, Device
from tinygrad.device import Buffer
# from tinygrad.nn.onnx import OnnxRunner
from tinygrad.frontend.onnx import OnnxRunner
if "JIT_BATCH_SIZE" not in os.environ:
os.environ["JIT_BATCH_SIZE"] = "0"
if "FLOAT16" not in os.environ:
os.environ["FLOAT16"] = "1"
if "OPT" not in os.environ:
os.environ["OPT"] = "99"
KEEP_BUFFERS = set()
original_reduce = Buffer.__reduce__
def stripped_reduce(self):
if id(self) in KEEP_BUFFERS:
return original_reduce(self)
return (self.__class__, (self.device, self.size, self.dtype))
Buffer.__reduce__ = stripped_reduce
def compile_model(onnx_path, output_path, input_shapes=None, input_types=None):
print(f"Compiling {onnx_path} -> {output_path}")
run_onnx = OnnxRunner(onnx_path)
if input_shapes is None:
input_shapes = {name: spec.shape for name, spec in run_onnx.graph_inputs.items()}
if input_types is None:
input_types = {name: spec.dtype for name, spec in run_onnx.graph_inputs.items()}
Tensor.manual_seed(100)
inputs = {k: Tensor(Tensor.randn(*shp, dtype=input_types[k]).mul(8).realize().numpy(), device='NPY') for k, shp in sorted(input_shapes.items())}
inputs = {k: v.to(Device.DEFAULT).realize() for k, v in inputs.items()}
print(f"Realized all {len(inputs)} inputs on {Device.DEFAULT}")
input_buf_ids = set()
for _, v in inputs.items():
if hasattr(v, '_buffer'):
try:
b = v._buffer()
if b is not None:
input_buf_ids.add(id(b))
except Exception:
pass
if "vision" in onnx_path:
onnx_jit = TinyJit(lambda **kwargs: next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32'), prune=True)
else:
onnx_jit = TinyJit(lambda **kwargs: [x.cast('float32').contiguous().realize() for x in run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values()], prune=True)
for i in range(3):
with Context(DEBUG=max(int(os.getenv("DEBUG", 0)), 2 if i == 2 else 1)):
res = onnx_jit(**inputs)
if isinstance(res, list):
for x in res:
x.numpy()
else:
res.numpy()
print(f"Captured {len(onnx_jit.captured.jit_cache)} kernels")
all_read_ids = set()
all_written_ids = set()
for ji in onnx_jit.captured.jit_cache:
if len(ji.bufs) > 0:
if ji.bufs[0] is not None:
all_written_ids.add(id(ji.bufs[0]))
for b in ji.bufs[1:]:
if b is not None:
all_read_ids.add(id(b))
weight_candidates = all_read_ids - all_written_ids
weight_ids = weight_candidates - input_buf_ids
print(f"Identified {len(weight_ids)} weight candidates (Read-Only & Not Input).")
total_weight_size = 0
marked_count = 0
for ji in onnx_jit.captured.jit_cache:
for b in ji.bufs:
if b is not None and id(b) in weight_ids:
if id(b) not in KEEP_BUFFERS:
KEEP_BUFFERS.add(id(b))
total_weight_size += b.size * b.dtype.itemsize
marked_count += 1
print(f"Preserving {marked_count} unique weight buffers.")
print(f"Total Preserved Weight Data Size: {total_weight_size / 1e6:.2f} MB")
with open(output_path, "wb") as f:
pickle.dump(onnx_jit, f)
print(f"Saved {output_path}, pkl size: {os.path.getsize(output_path)/1e6:.2f} MB")
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python compile_split_tinygrad.py <input_onnx> <output_pkl>")
sys.exit(1)
input_onnx = sys.argv[1]
output_pkl = sys.argv[2]
compile_model(input_onnx, output_pkl)

View File

@@ -1,161 +0,0 @@
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan
def interp_vec(t_out, t_in, vec):
# vec shape (N, 3), output (M, 3)
return np.stack([np.interp(t_out, t_in, vec[:, i]) for i in range(3)], axis=1)
def fill_alpamayo_msg(modelV2, net_outputs, frame_id, frame_drop_ratio, timestamp_eof, CP, lat_delay, v_ego):
modelV2.frameId = frame_id
modelV2.frameIdExtra = frame_id
modelV2.timestampEof = timestamp_eof
modelV2.frameDropPerc = frame_drop_ratio * 100.0
modelV2.init('laneLines', 4)
modelV2.init('roadEdges', 2)
modelV2.init('laneLineProbs', 4)
modelV2.init('roadEdgeStds', 2)
for i in range(4):
l = modelV2.laneLines[i]
l.t = [0.0]
l.x = [0.0]
l.y = [0.0]
l.z = [0.0]
modelV2.laneLineProbs[i] = 0.0
for i in range(2):
e = modelV2.roadEdges[i]
e.t = [0.0]
e.x = [0.0]
e.y = [0.0]
e.z = [0.0]
modelV2.roadEdgeStds[i] = 1.0
leads = modelV2.init('leadsV3', 1)
lead = leads[0]
pred_lead = net_outputs['pred_lead'][0]
prob_logit = float(pred_lead[0])
dist_pred = float(pred_lead[1] * 100.0)
dist_sigma = float(np.exp(pred_lead[2]))
v_rel_pred = float(pred_lead[3])
v_sigma = float(np.exp(pred_lead[4]))
a_rel_pred = float(pred_lead[5])
a_sigma = float(np.exp(pred_lead[6]))
prob = float(1.0 / (1.0 + np.exp(-prob_logit)))
lead.prob = prob
lead.probTime = 0.0
# X(t) = X0 + V_rel*t + 0.5*A_rel*t^2
T = ModelConstants.T_IDXS
lead.t = list(T)
lead.x = [float(dist_pred + v_rel_pred * t + 0.5 * a_rel_pred * t**2) for t in T]
lead.v = [float(v_ego + v_rel_pred + a_rel_pred * t) for t in T]
a_ego = net_outputs['acceleration'][0, 0, 0] # T=0 ego accel estimate (x component)
lead.a = [float(a_ego + a_rel_pred)] * len(T)
lead.y = [0.0] * len(T)
lead.xStd = [max(0.5, dist_sigma * 100.0)] * len(T)
lead.yStd = [1.0] * len(T)
lead.vStd = [max(0.1, v_sigma)] * len(T)
lead.aStd = [max(0.1, a_sigma)] * len(T)
modelV2.meta.engagedProb = 1.0
desire_pred = [0.0] * 8
if 'pred_light' in net_outputs:
red_prob = float(1.0 / (1.0 + np.exp(-net_outputs['pred_light'][0, 1] + net_outputs['pred_light'][0, 0])))
desire_pred[4] = red_prob
modelV2.meta.desirePrediction = desire_pred
modelV2.meta.desireState = [0.0] * 8
reasoning_error = net_outputs.get('consistency_error', 0.0)
if reasoning_error < 0.5:
modelV2.confidence = "green"
elif reasoning_error < 1.5:
modelV2.confidence = "yellow"
else:
modelV2.confidence = "red"
ALPAMAYO_T_IDXS = np.arange(1, 65) * 0.1 # 64 steps at .1s intervals
t_idxs = ModelConstants.T_IDXS
t_all = np.concatenate(([0.0], ALPAMAYO_T_IDXS)) # this model starts at t=0.1 so if we prepend 0.0 and interpolate for t=now it should match op
pos_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs['position'][0])))
pos_std_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs.get('position_std', np.ones((64, 3)) * 0.1))))
vel_interp = interp_vec(t_idxs, t_all, np.vstack(([v_ego, 0.0, 0.0], net_outputs['velocity'][0])))
acc_interp = interp_vec(t_idxs, t_all, np.vstack((net_outputs['acceleration'][0][0], net_outputs['acceleration'][0])))
rot_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs['orientation'][0])))
rate_interp = interp_vec(t_idxs, t_all, np.vstack((net_outputs['orientation_rate'][0][0], net_outputs['orientation_rate'][0])))
# https://www.mathworks.com/help/vdynblks/ug/coordinate-systems-in-vehicle-dynamics-blockset.html
# following SAE J670 and ISO 8855, for sunnymayo model: x is forward (f), y is left (lat), z is up/vert
# Openpilot Modelv2 and camerad expects SAE J670: x is forward, y is right, z is down
modelV2.position.t = t_idxs # time, obviously
modelV2.position.x = pos_interp[:, 0].tolist() # f dist
modelV2.position.y = (-pos_interp[:, 1]).tolist() # lat offset (Flip L->R)
modelV2.position.z = (-pos_interp[:, 2]).tolist() # vert offset (Flip U->D) (elevation)
modelV2.position.xStd = pos_std_interp[:, 0].tolist()
modelV2.position.yStd = pos_std_interp[:, 1].tolist()
modelV2.position.zStd = pos_std_interp[:, 2].tolist()
modelV2.velocity.t = t_idxs
modelV2.velocity.x = vel_interp[:, 0].tolist() # f vel (vego)
modelV2.velocity.y = (-vel_interp[:, 1]).tolist() # lat vel (curvature)
modelV2.velocity.z = (-vel_interp[:, 2]).tolist() # vert vel
modelV2.acceleration.t = t_idxs
modelV2.acceleration.x = acc_interp[:, 0].tolist() # f accel (aego)
modelV2.acceleration.y = (-acc_interp[:, 1]).tolist() # lat accel
modelV2.acceleration.z = (-acc_interp[:, 2]).tolist() # vert accel
modelV2.orientation.t = t_idxs
modelV2.orientation.x = rot_interp[:, 0].tolist() # roll (treated as 0)
modelV2.orientation.y = (-rot_interp[:, 1]).tolist() # pitch (from z-slope)
modelV2.orientation.z = (-rot_interp[:, 2]).tolist() # yaw (heading)
modelV2.orientationRate.t = t_idxs
modelV2.orientationRate.x = rate_interp[:, 0].tolist() # roll rate
modelV2.orientationRate.y = (-rate_interp[:, 1]).tolist() # pitch rate (Flip U->D)
modelV2.orientationRate.z = (-rate_interp[:, 2]).tolist() # yaw rate (Flip L->R)
long_action_t = CP.longitudinalActuatorDelay + DT_MDL
desired_accel, should_stop = get_accel_from_plan(vel_interp[:, 0], acc_interp[:, 0], t_idxs, action_t=long_action_t)
modelV2.action.desiredAcceleration = float(desired_accel)
modelV2.action.shouldStop = bool(should_stop)
lat_action_t = lat_delay + DT_MDL
desired_curvature = get_curvature_from_plan(-rot_interp[:, 2], -rate_interp[:, 2], t_idxs, vego=v_ego, action_t=lat_action_t)
modelV2.action.desiredCurvature = float(desired_curvature)
def fill_pose_msg(camera_odometry, net_outputs, frame_id, timestamp_eof):
camera_odometry.frameId = frame_id
camera_odometry.timestampEof = timestamp_eof
trans = net_outputs['velocity'][0, 0].copy()
trans[1] *= -1.0
trans[2] *= -1.0
camera_odometry.trans = trans.tolist()
std_val = float(max(0.01, net_outputs.get('consistency_error', 0.1)))
camera_odometry.transStd = [std_val, std_val, std_val]
rot = net_outputs['orientation_rate'][0, 0].copy()
rot[1] *= -1.0
rot[2] *= -1.0
camera_odometry.rot = rot.tolist()
rot_std = std_val * 0.1
camera_odometry.rotStd = [rot_std, rot_std, rot_std]

View File

@@ -1,51 +0,0 @@
import numpy as np
from dataclasses import dataclass
from openpilot.common.params import Params
@dataclass
class AlpamayoDesire:
DRIVE_SAFELY = 0
TURN_LEFT = 2
TURN_RIGHT = 1
DRIVE_FAST = 3
STOP = 4
class InputIDHelper:
def __init__(self):
self.current_ids = np.zeros((1, 16), dtype=np.int64)
self.desire = AlpamayoDesire.DRIVE_SAFELY
self.params = Params()
self.drive_fast = False
self.msg_count = -1
def update_params(self):
if self.msg_count % 60 == 0:
self.drive_fast = self.params.get_bool("AlpamayoDriveFast")
self.msg_count += 1
def update(self, sm):
self.update_params()
if sm is None:
return self.current_ids
left_blinker = False
right_blinker = False
if sm.seen['carState']:
left_blinker = sm['carState'].leftBlinker
right_blinker = sm['carState'].rightBlinker
# Priority: STOP (TODO) > Turn > Drive Fast > Drive Safely
new_desire = AlpamayoDesire.DRIVE_SAFELY
if left_blinker:
new_desire = AlpamayoDesire.TURN_LEFT
elif right_blinker:
new_desire = AlpamayoDesire.TURN_RIGHT
elif self.drive_fast:
new_desire = AlpamayoDesire.DRIVE_FAST
self.desire = new_desire
self.current_ids.fill(self.desire)
return self.current_ids

View File

@@ -1,60 +0,0 @@
from tinygrad.tensor import Tensor
def action_to_traj(action: Tensor, v0: Tensor, dt: float = 0.1):
"""
This function is a lightweight tinygrad transformation of the unicycle accel physics model based on Nvidia's
unicycle model https://github.com/NVlabs/alpamayo/blob/main/src/alpamayo_r1/action_space/unicycle_accel_curvature.py
Integrate action (accel, kappa) to trajectory (x, y, theta)
Args:
action: (B, T, 2) [accel, kappa]
v0: (B,) Initial velocity
dt: Time step
Returns:
res: Dict containing position, velocity, acceleration, orientation, orientation_rate
"""
B, T, _ = action.shape
ACCEL_MEAN = 0.02902695
ACCEL_STD = 0.68104267
CURV_MEAN = 0.00026922
CURV_STD = 0.02614828
accel = action[..., 0] * ACCEL_STD + ACCEL_MEAN
kappa = action[..., 1] * CURV_STD + CURV_MEAN
# v_{t+1} = v_t + a_t * dt
v_diff = accel * dt
v_seq = v_diff.cumsum(axis=1) + v0.reshape(B, 1) # cumulative sum over T dimension (axis 1)
velocity = v0.reshape(B, 1).cat(v_seq, dim=1)
# theta_{t+1} = theta_t + kappa_t * (v_t * dt + 0.5 * a_t * dt^2)
dt_2_term = 0.5 * (dt**2)
dtheta = kappa * (velocity[:, :-1] * dt + accel * dt_2_term)
theta_seq = dtheta.cumsum(axis=1)
theta = Tensor.zeros(B, 1, device=action.device, dtype=action.dtype).cat(theta_seq, dim=1)
# trapezoidal euler
half_dt = 0.5 * dt
v_cos = velocity * theta.cos()
v_sin = velocity * theta.sin()
dx = (v_cos[:, :-1] + v_cos[:, 1:]) * half_dt
dy = (v_sin[:, :-1] + v_sin[:, 1:]) * half_dt
x = dx.cumsum(axis=1)
y = dy.cumsum(axis=1)
res = {}
res['action'] = accel.stack(kappa, dim=-1) # raw model output
# (x, y, 0)
res['position'] = x.stack(y, Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
# (vx, vy, 0)
res['velocity'] = v_cos[:, 1:].stack(v_sin[:, 1:], Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
# ax = accel * cos(theta), ay = accel * sin(theta), 0
res['acceleration'] = (accel * theta[:, 1:].cos()).stack(accel * theta[:, 1:].sin(), Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
# (0, 0, theta)
res['orientation'] = Tensor.zeros(B, T, device=action.device, dtype=action.dtype).stack(Tensor.zeros(B, T, device=action.device, dtype=action.dtype), theta[:, 1:], dim=-1)
# (0, 0, dtheta/dt)
res['orientation_rate'] = Tensor.zeros(B, T, device=action.device, dtype=action.dtype).stack(Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dtheta / dt, dim=-1)
return res

View File

@@ -1,19 +0,0 @@
import pickle
from pathlib import Path
from openpilot.common.swaglog import cloudlog
def load_compiled_model(model_name: str = "student"):
pkl_path = Path(__file__).parent / "distilled_models" / f"{model_name}_tinygrad.pkl"
if not pkl_path.exists():
cloudlog.error(f"Compiled model not found at {pkl_path}")
return None
try:
with open(pkl_path, "rb") as f:
model_run = pickle.load(f)
return model_run
except Exception as e:
cloudlog.error(f"Failed to load compiled Tinygrad model: {e}")
return None

View File

@@ -1,344 +0,0 @@
import time
import numpy as np
import os
import platform
from setproctitle import setproctitle
from openpilot.system.hardware import TICI
if TICI:
os.environ['DEV'] = 'QCOM'
elif platform.system() == "Darwin":
os.environ['DEV'] = "METAL"
else:
os.environ['DEV'] = 'CPU'
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
os.environ['AMD_IFACE'] = 'USB'
from tinygrad.tensor import Tensor
from tinygrad.dtype import dtypes
import cereal.messaging as messaging
from cereal import car
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.transformations.model import bigmodel_frame_from_calib_frame
from openpilot.common.transformations.camera import DEVICE_CAMERAS, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler
from openpilot.common.realtime import Ratekeeper
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.sunnypilot.modeld_sunny.kinematic_model import action_to_traj
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_sunny.loader import load_compiled_model
from openpilot.sunnypilot.modeld_sunny.input_id_helper import InputIDHelper
from openpilot.sunnypilot.modeld_sunny.fill_model_msg import fill_alpamayo_msg, fill_pose_msg
PROCESS_NAME = "selfdrive.modeld.openpilot.sunnypilot.modeld_sunny"
class FrameMeta:
frame_id: int = 0
timestamp_sof: int = 0
timestamp_eof: int = 0
def __init__(self, vipc=None):
if vipc is not None:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
def safe_exp(x):
return np.exp(np.clip(x, -np.inf, 11))
def softmax(x, axis=-1):
x = x - np.max(x, axis=axis, keepdims=True)
x = safe_exp(x)
return x / np.sum(x, axis=axis, keepdims=True)
class AlpamayoModelD:
def __init__(self, context: CLContext):
self.params = Params()
self.context = context
self.model_vision = load_compiled_model("student_vision")
self.model_policy = load_compiled_model("student_policy")
self.model_loaded = self.model_vision is not None and self.model_policy is not None
self.vision_input_names = ['road', 'wide']
self.vision_input_shapes = {
'road': (1, 3, 512, 1024),
'wide': (1, 3, 512, 1024)
}
self.frames = {name: DrivingModelFrame(context, 1024, 512, buffer_length=4) for name in self.vision_input_names}
self.history_buffer = np.zeros((16, 3), dtype=np.float32)
self.logic_pulse = np.zeros((1, 2048), dtype=np.float32)
def run(self, bufs, transforms, inputs, prepare_only):
if prepare_only:
return None
if not hasattr(self, 'vision_inputs'):
self.vision_inputs = {}
imgs_cl = {n: self.frames[n].prepare(bufs[n], transforms[n].flatten()) for n in self.vision_input_names if bufs.get(n)}
if TICI and not USBGPU:
for k, v in imgs_cl.items():
if k not in self.vision_inputs:
self.vision_inputs[k] = qcom_tensor_from_opencl_address(v.mem_address, self.vision_input_shapes[k], dtype=dtypes.uint8)
else:
for k, v in imgs_cl.items():
self.vision_inputs[k] = Tensor(self.frames[k].buffer_from_cl(v).reshape(self.vision_input_shapes[k]), dtype=dtypes.uint8).realize()
img_t = Tensor.stack([self.vision_inputs['wide'].cast(dtypes.float32) / 255.0,
self.vision_inputs['road'].cast(dtypes.float32) / 255.0], dim=1).unsqueeze(0)
vis_res = self.model_vision(
history=Tensor(inputs["history"]).contiguous().realize(),
img=img_t.contiguous().realize(),
input_ids=Tensor(inputs["input_ids"]).contiguous().realize(),
logic_pulse=Tensor(inputs["logic_pulse"]).contiguous().realize()
)
context = vis_res.contiguous().realize()
x_input = Tensor.zeros(1, 64, 2, device=os.environ.get("DEV"), dtype=dtypes.float32)
v_mu, v_std, pred_pulse, state_mu, state_std, pred_light, pred_lead, hypot_logits = self.model_policy(
context=context,
noisy_action=x_input.contiguous().realize(),
t=Tensor(np.array([[0.0]], dtype=np.float32)).contiguous().realize(), # t=0
traffic=Tensor(inputs["traffic_convention"]).contiguous().realize()
)
weights = softmax(hypot_logits.numpy(), axis=1) # (B, M)
winner_idx = np.argmax(weights[0])
v_winner = v_mu[:, winner_idx]
state_winner = state_mu[:, winner_idx]
state_std_winner = state_std[:, winner_idx]
outputs_tg = action_to_traj(v_winner, Tensor([inputs["v_ego"]], dtype=dtypes.float32), dt=0.1)
outputs = {k: v.numpy() for k, v in outputs_tg.items()}
outputs.update({
"pred_pulse": pred_pulse.numpy(),
"pred_light": pred_light[0:1].numpy(),
"pred_lead": pred_lead[0:1].numpy(),
"weights": weights[0]
})
# Inject world positions for Z/Pitch
pos_world = state_winner[0].numpy()
pos_std = np.exp(state_std_winner[0].numpy())
outputs["position"][0, :, 2] = pos_world[:, 2]
outputs["position_std"] = pos_std # log_sigma -> sigma
d_pos = np.diff(outputs["position"][0], axis=0, prepend=np.zeros((1, 3)))
d_dist = np.maximum(np.linalg.norm(d_pos[:, :2], axis=1), 1e-4)
pitch = np.arctan2(np.diff(pos_world[:, 2], prepend=0.0), d_dist)
outputs["orientation"][0, :, 1] = pitch
outputs["orientation_rate"][0, :, 1] = np.diff(pitch, prepend=0.0) / 0.1
outputs["velocity"][0, :, 2] = np.linalg.norm(outputs["velocity"][0, :, :2], axis=1) * np.tan(pitch)
outputs["consistency_error"] = float(np.mean(np.linalg.norm(outputs["position"][0] - pos_world, axis=1)))
return outputs
def main():
setproctitle(PROCESS_NAME)
config_realtime_process(7, 54)
# Loop runs at 20Hz to match camera acquisition.
# Model inference runs at 10Hz via frame skipping.
rk = Ratekeeper(1.0 / DT_MDL)
cl_context = CLContext()
modeld = AlpamayoModelD(cl_context)
# Load CarParams
cloudlog.warning("Modeld: Waiting for CarParams...")
CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams)
cloudlog.warning("Modeld: Got CarParams")
camera_offset_helper = CameraOffsetHelper()
input_id_helper = InputIDHelper()
if modeld.model_loaded:
cloudlog.warning("Modeld: Successfully loaded compiled student model.")
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "liveDelay", "livePose", "driverMonitoringState"])
# VisionIPC Clients
while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False)
if available_streams:
use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams
main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams
break
time.sleep(.1)
vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD
vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context)
vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context)
cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}")
while not vipc_client_main.connect(False):
time.sleep(0.1)
while use_extra_client and not vipc_client_extra.connect(False):
time.sleep(0.1)
cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})")
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
model_transform_main = np.zeros((3, 3), dtype=np.float32)
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
buf_main, buf_extra = None, None
meta_main = FrameMeta()
meta_extra = FrameMeta()
# filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / 20.0)
last_vipc_frame_id = 0
run_count = 0
lat_delay = 0.0
live_calib_seen = False
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
buf_main = vipc_client_main.recv()
meta_main = FrameMeta(vipc_client_main)
if buf_main is None:
break
if buf_main is None:
cloudlog.debug("vipc_client_main no frame")
continue
if use_extra_client:
# Keep receiving extra frames until frame id matches main camera
while True:
buf_extra = vipc_client_extra.recv()
meta_extra = FrameMeta(vipc_client_extra)
if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
break
if buf_extra is None:
cloudlog.debug("vipc_client_extra no frame")
continue
if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000:
cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\
extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})")
else:
# Use single camera
buf_extra = buf_main
meta_extra = meta_main
# 10Hz Execution Check (Skip odd frames)
# We use main camera frameId as the clock
if meta_main.frame_id % 2 != 0:
last_vipc_frame_id = meta_main.frame_id
continue
sm.update(0)
v_ego = sm['carState'].vEgo if sm.seen['carState'] else 0.0
yaw_rate = 0.0
if sm.seen['livePose'] and sm['livePose'].angularVelocityDevice.valid:
yaw_rate = sm['livePose'].angularVelocityDevice.z
if sm.frame % 60 == 0:
lat_delay = get_lat_delay(modeld.params, sm["liveDelay"].lateralDelay)
camera_offset_helper.set_offset(modeld.params.get("CameraOffset", return_default=True))
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
calib_from_bigmodel = np.linalg.inv(bigmodel_frame_from_calib_frame[:, :3])
device_from_calib = rot_from_euler(device_from_calib_euler)
camera_from_calib_main = (dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics) @ view_frame_from_device_frame @ device_from_calib
model_transform_main = camera_from_calib_main @ calib_from_bigmodel
camera_from_calib_extra = dc.ecam.intrinsics @ view_frame_from_device_frame @ device_from_calib
model_transform_extra = camera_from_calib_extra @ calib_from_bigmodel
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
live_calib_seen = True
# Track dropped frames
vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1)
frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10))
if run_count < 10: # let frame drops warm up
frame_dropped_filter.x = 0.
frames_dropped = 0.
run_count = run_count + 1
frame_drop_ratio = frames_dropped / (1 + frames_dropped)
prepare_only = vipc_dropped_frames > 0
if prepare_only:
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
bufs = {'road': buf_main, 'wide': buf_extra}
transforms = {'road': model_transform_main, 'wide': model_transform_extra}
dt = 0.1
d_yaw = yaw_rate * dt
d_pos = v_ego * dt * np.array([np.cos(d_yaw/2), np.sin(d_yaw/2)])
rot = np.array([[np.cos(-d_yaw), -np.sin(-d_yaw)], [np.sin(-d_yaw), np.cos(-d_yaw)]])
modeld.history_buffer[:, :2] = (modeld.history_buffer[:, :2] - d_pos) @ rot.T
modeld.history_buffer[:, 2] -= d_yaw
modeld.history_buffer = np.roll(modeld.history_buffer, -1, axis=0)
modeld.history_buffer[-1] = 0.
hist_input = modeld.history_buffer.copy()
hist_input[:, 1] *= -1.0
hist_input[:, 2] *= -1.0
yaws_fixed = hist_input[:, 2]
cos_y, sin_y = np.cos(yaws_fixed), np.sin(yaws_fixed)
zeros, ones = np.zeros_like(cos_y), np.ones_like(cos_y)
rot_flat = np.column_stack([cos_y, -sin_y, zeros, sin_y, cos_y, zeros, zeros, zeros, ones])
inputs = {
'input_ids': input_id_helper.update(sm),
'history': np.column_stack([hist_input[:, :2], zeros, rot_flat])[None, ...].astype(np.float32),
'logic_pulse': modeld.logic_pulse,
'traffic_convention': np.array([[0.0, 1.0]] if sm["driverMonitoringState"].isRHD else [[1.0, 0.0]], dtype=np.float32),
'v_ego': v_ego
}
t0 = time.monotonic()
outputs = modeld.run(bufs, transforms, inputs, prepare_only)
t1 = time.monotonic()
if not prepare_only:
cloudlog.warning(f"Modeld: Inference took {(t1-t0)*1000:.2f} ms")
last_vipc_frame_id = meta_main.frame_id
if outputs is not None:
modeld.logic_pulse[:] = outputs["pred_pulse"]
model_msg = messaging.new_message('modelV2')
drivingdata_msg = messaging.new_message('drivingModelData')
posenet_msg = messaging.new_message('cameraOdometry')
fill_alpamayo_msg(model_msg.modelV2, outputs, meta_main.frame_id, frame_drop_ratio, meta_main.timestamp_eof, CP, lat_delay, v_ego)
model_msg.valid = live_calib_seen and (vipc_dropped_frames < 1)
fill_pose_msg(posenet_msg.cameraOdometry, outputs, meta_main.frame_id, meta_main.timestamp_eof)
posenet_msg.valid = live_calib_seen and (vipc_dropped_frames < 1)
drivingdata_msg.drivingModelData.frameId = meta_main.frame_id
pm.send('drivingModelData', drivingdata_msg)
pm.send('cameraOdometry', posenet_msg)
pm.send('modelV2', model_msg)
rk.keep_time()
if __name__ == "__main__":
main()

View File

@@ -1,83 +0,0 @@
import pytest
import numpy as np
from cereal import car
import cereal.messaging as messaging
from msgq.visionipc import VisionIpcServer, VisionIpcClient, VisionStreamType
from sunnypilot.modeld_sunny.modeld import AlpamayoModelD
from sunnypilot.modeld_v2.models.commonmodel_pyx import CLContext
from sunnypilot.modeld_sunny.fill_model_msg import fill_alpamayo_msg
@pytest.fixture(scope="module")
def cl_context():
return CLContext()
@pytest.fixture(scope="module")
def modeld(cl_context):
print("Initializing AlpamayoModelD...")
return AlpamayoModelD(cl_context)
@pytest.fixture(scope="function")
def vipc_server():
server_name = "camerad_test"
server = VisionIpcServer(server_name)
server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 1, 1024, 512)
server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 1, 1024, 512)
server.start_listener()
yield server
def test_modeld(cl_context, modeld, vipc_server):
v_ego = 20.0
inputs = {
'input_ids': np.zeros((1, 16), dtype=np.int64),
'history': np.zeros((1, 16, 12), dtype=np.float32),
'logic_pulse': modeld.logic_pulse,
'traffic_convention': np.array([[1.0, 0.0]], dtype=np.float32),
'v_ego': v_ego
}
server_name = "camerad_test"
client_road = VisionIpcClient(server_name, VisionStreamType.VISION_STREAM_ROAD, False, cl_context)
assert client_road.connect(True), "Road client failed to connect"
client_wide = VisionIpcClient(server_name, VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context)
assert client_wide.connect(True), "Wide client failed to connect"
# NV12 size for 1024x512 = 1024*512 * 1.5 = 786432
yuv_data = b'\x00' * 786432
vipc_server.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data)
vipc_server.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, yuv_data)
buf_road = client_road.recv()
buf_wide = client_wide.recv()
assert buf_road is not None
assert buf_wide is not None
bufs = {'road': buf_road, 'wide': buf_wide}
transforms = {'road': np.eye(3, dtype=np.float32), 'wide': np.eye(3, dtype=np.float32)}
outputs = modeld.run(bufs, transforms, inputs, False)
assert outputs is not None
assert outputs["position"].shape == (1, 64, 3)
assert outputs["velocity"].shape == (1, 64, 3)
assert outputs["acceleration"].shape == (1, 64, 3)
assert outputs["orientation"].shape == (1, 64, 3)
assert "pred_pulse" in outputs
assert "pred_light" in outputs
assert "pred_lead" in outputs
assert np.all(np.isfinite(outputs["position"])), "Position contains NaN/Inf"
assert np.all(np.isfinite(outputs["velocity"])), "Velocity contains NaN/Inf"
assert "consistency_error" in outputs
assert outputs["consistency_error"] >= 0.0
model = messaging.new_message('modelV2')
CP = car.CarParams.new_message()
CP.longitudinalActuatorDelay = 0.2
fill_alpamayo_msg(model.modelV2, outputs, 12345, 0.0, 1e9, CP, 0.1, v_ego)
# these just ensure that the model should outputs same action for the same black pixels
assert model.modelV2.action.desiredAcceleration == pytest.approx(-6.75, abs=1e-2)
assert model.modelV2.action.desiredCurvature == pytest.approx(-0.05, abs=1e-2)
assert not model.modelV2.action.shouldStop
assert model.modelV2.frameId == 12345

View File

@@ -60,7 +60,7 @@ def tg_compile(flags, model_name):
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
if File(f"models/{model_name}.onnx").exists():
flags = {
'larch64': 'DEV=QCOM',
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
tg_compile(flags, model_name)

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