mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-19 16:52:06 +08:00
Compare commits
462 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87a67ac195 | |||
| dc1edf294e | |||
| aef9a95c42 | |||
| 0ccded8294 | |||
| c1614d197a | |||
| 15dac3d906 | |||
| 98ecfafcdd | |||
| 0306e59ac1 | |||
| 2d80f2db96 | |||
| 795ed7afb5 | |||
| 206368ec68 | |||
| aa141521fc | |||
| 6e421989ab | |||
| 9c3a73b4cf | |||
| d10349721c | |||
| b48214acd2 | |||
| 8adbd56acd | |||
| 65db08f4d1 | |||
| a1e305333f | |||
| c14b81585e | |||
| f3d8b24bf4 | |||
| 880ed98ffc | |||
| f1025f6ee9 | |||
| 08e85808c5 | |||
| cb03d08397 | |||
| 90cbb09482 | |||
| 9c19ec8409 | |||
| fc253fe1ee | |||
| d72a01d739 | |||
| f93b3f51c9 | |||
| 3d08a5048b | |||
| 9ee66008db | |||
| 6a257fe2de | |||
| dad7bb53a2 | |||
| 47ba86af33 | |||
| d106c192f2 | |||
| 98ffbe1308 | |||
| d09f74612f | |||
| 3af0d6e87f | |||
| 584269fced | |||
| 1dc5741e75 | |||
| b69da9e5ea | |||
| 9689de426b | |||
| 0ccd55a6b5 | |||
| 124eb42758 | |||
| 85404c184b | |||
| ed42cfe699 | |||
| 3099f4f12d | |||
| 8fceb9d957 | |||
| 48dc9dbb69 | |||
| 56ca486fe9 | |||
| 799e819e58 | |||
| aaac1c79d0 | |||
| f46de2d0d5 | |||
| d7fa10a827 | |||
| 22b010f674 | |||
| dcaf84d04c | |||
| 3a82a0797a | |||
| 365e978b42 | |||
| dd074cb6ef | |||
| 2d1f3833e4 | |||
| d4185a5d57 | |||
| 1262fca36b | |||
| ba176a6581 | |||
| 63e5d0a476 | |||
| e28dd1e1aa | |||
| 890b1cf512 | |||
| 1633641055 | |||
| 2dcb67091f | |||
| fb34601d5a | |||
| c1d3ae427b | |||
| 2ab45b552d | |||
| 8c1d59fecd | |||
| 43b4e4e271 | |||
| b6bcc8cca3 | |||
| cde88fd8ed | |||
| e5a7deb6ad | |||
| 10100e34e1 | |||
| 2d31b422c8 | |||
| 864c811ef6 | |||
| 89919c8832 | |||
| 906e9d7a80 | |||
| cfb8f3ae24 | |||
| 0cc5e56192 | |||
| 1a62ae821e | |||
| dc5f5eaf65 | |||
| ee8970dc42 | |||
| 0a44b48e21 | |||
| 36e53c7394 | |||
| 38eb400e41 | |||
| 4b5de0eddb | |||
| 5198b1b079 | |||
| e8a11591a8 | |||
| cbc8f98682 | |||
| ecdcb5d0c6 | |||
| c7494aed0f | |||
| 215ef16803 | |||
| 350b846d3a | |||
| 9ce9920ff7 | |||
| 1c0b087105 | |||
| 137d4b89b4 | |||
| 2cc4885a2e | |||
| 736e1fa7b7 | |||
| 177c7f1cf3 | |||
| 9bf904e8a6 | |||
| 5ea5f6f267 | |||
| 525b6e48e9 | |||
| c7b115b68e | |||
| b6dd2d14db | |||
| 7d4e5bedaf | |||
| 1063114408 | |||
| 958b4df69f | |||
| 62aef9cd34 | |||
| f57617c944 | |||
| c4a0e57046 | |||
| 76c5cb6d87 | |||
| fc4e5007fd | |||
| af24fd6842 | |||
| 72998034e6 | |||
| cefb344183 | |||
| d17e80ad94 | |||
| c2b7087723 | |||
| 81b37712f1 | |||
| 18cd3633e5 | |||
| 9c6a4d4a57 | |||
| 1a4c48249b | |||
| 002a22a097 | |||
| 9f20eb8ce6 | |||
| 2e636458a6 | |||
| 47d0a95fd6 | |||
| 5d142326f5 | |||
| ef9683ee79 | |||
| 5f5e3668eb | |||
| 8c07958f6f | |||
| ca1ce9bcc9 | |||
| 8a77534d02 | |||
| 73ed45f9d7 | |||
| 2d6df2e125 | |||
| e754b738ad | |||
| 1dadb3fcc9 | |||
| 4e88245745 | |||
| debc9bf7cf | |||
| e03673485b | |||
| 6efe4e1998 | |||
| ff6ed7055d | |||
| 6c85e2c697 | |||
| 2d0340cefd | |||
| a974deeb59 | |||
| cf5bb4e16e | |||
| 0d4b0ee116 | |||
| 03cb3e9dc0 | |||
| 29f15dc8ed | |||
| 31a5a3b3c0 | |||
| 2a4b348497 | |||
| 3ef3aceb4b | |||
| f0dd0b5c8c | |||
| 3d8763b3ce | |||
| b2427a5f20 | |||
| 94ca077e69 | |||
| e92e59ca78 | |||
| e0cabc1174 | |||
| 5e2f142704 | |||
| 2beb0ffad1 | |||
| fa373af9b5 | |||
| 7909716c1f | |||
| c1cb971bca | |||
| 538ec25ad9 | |||
| 17152484c2 | |||
| 954b567b9b | |||
| 6061476d8e | |||
| ad903aeaa1 | |||
| c8c1b0f781 | |||
| 534f096bb8 | |||
| 7da36b2470 | |||
| f2db7f7665 | |||
| 40a1af97b9 | |||
| 53ff5413cd | |||
| dc889587ce | |||
| ff4cc96a81 | |||
| 3b1ada64be | |||
| 6a08186434 | |||
| cf2b033c79 | |||
| 9fbef36c6b | |||
| 7b28c2f59a | |||
| 99d954de10 | |||
| b28f33481c | |||
| 589e33f665 | |||
| 39342d7b5e | |||
| 6486ab6cab | |||
| ab234c72a3 | |||
| 485c7b2725 | |||
| 4861d15056 | |||
| 1e73025f86 | |||
| 378212e5ab | |||
| 4f52f3f3c5 | |||
| a0d48b6c63 | |||
| b14270bd71 | |||
| 8f720a54f6 | |||
| 2c41dbc472 | |||
| a8660b5b4f | |||
| 4ccafff123 | |||
| 856f8d3d47 | |||
| 00e20f1524 | |||
| 215acefbb4 | |||
| c33c9ff22a | |||
| 99fdd59042 | |||
| 5af1099fbf | |||
| f983df0c70 | |||
| 936740201c | |||
| 4489517eeb | |||
| b1b7c505a1 | |||
| a2e7f3788f | |||
| d2bb8fe537 | |||
| 5289b08bcf | |||
| b763f7aac1 | |||
| 450fcd4d55 | |||
| 551b4dea31 | |||
| bd269defb3 | |||
| 399ed08926 | |||
| 90f02040fe | |||
| 8423ecedb1 | |||
| dd1479ed82 | |||
| cc8f6eadfe | |||
| f82845ff42 | |||
| efcc5ccd15 | |||
| 6aac50ab56 | |||
| 091bce4a3a | |||
| 088f6aa407 | |||
| 211c8adcce | |||
| 9b2f7341d8 | |||
| 650946cd2a | |||
| 9801e486d9 | |||
| 3381192297 | |||
| b2e3dd17ea | |||
| 01715f6f9a | |||
| 8720e5d712 | |||
| 8752093801 | |||
| 3c957c6e9d | |||
| 3ef5037c16 | |||
| fe5366e5b2 | |||
| 1ecb0b0f66 | |||
| 51e455db79 | |||
| dc6672fa80 | |||
| 07b8e7783d | |||
| f17b0f200c | |||
| ad9bde8b1f | |||
| 8cf9f9fe23 | |||
| 713985d823 | |||
| 088f9d0b59 | |||
| 53bf5b0d41 | |||
| 8c33592628 | |||
| 3bbb33f6bd | |||
| 7534b2a160 | |||
| b28425b8c3 | |||
| 1f5e0b6f68 | |||
| 646f6a1006 | |||
| cc683f2040 | |||
| 18e8f648c2 | |||
| 821e4da2c7 | |||
| 13d98fd2d5 | |||
| 92cd656c68 | |||
| 727a750b34 | |||
| 5dabb678ce | |||
| ef988aca28 | |||
| 5bd9549bd1 | |||
| 64f3759fd0 | |||
| 3481702715 | |||
| c9781ee31d | |||
| d71d2bd2d0 | |||
| 702bebf176 | |||
| 25da8e9d44 | |||
| 845f6ec8cf | |||
| e1ad4daf8d | |||
| 783b717af8 | |||
| 65e1fd299e | |||
| b29b1964ba | |||
| 80a8df0643 | |||
| d9fc6c0086 | |||
| cb612a4b90 | |||
| 36d77debd0 | |||
| 530ad2925d | |||
| ec33519dc7 | |||
| 2fd4b53aaf | |||
| a2c4fe1c90 | |||
| 3e56612990 | |||
| 75858673c4 | |||
| 57223958b5 | |||
| 3553a754a4 | |||
| f290fb1e05 | |||
| 0c64818f52 | |||
| c44548ba0f | |||
| 59bddfba8d | |||
| 8a1fcd8991 | |||
| 3546b625e7 | |||
| 87443cd34d | |||
| 5f0e9fce61 | |||
| a2cce7f897 | |||
| f4041dc1f0 | |||
| 8e3757ac87 | |||
| 41fa0cdf82 | |||
| 692f5fdd72 | |||
| de0a1e66d8 | |||
| 129445cd1d | |||
| 13d0aefd7c | |||
| 5f7b05e808 | |||
| 32f65bae55 | |||
| 49d9b8bb00 | |||
| b3eba70b7a | |||
| cec7a5dc98 | |||
| 14993f58e3 | |||
| e8a17b4963 | |||
| fb77212221 | |||
| aa7f6973c0 | |||
| c2af5a82ff | |||
| 348114e5bd | |||
| a6e28ac2ee | |||
| 0e6f78a656 | |||
| 2305fb59a2 | |||
| cc816043c1 | |||
| b6dbb0fd8d | |||
| fdcf8b592e | |||
| 4ff77a4752 | |||
| f04ee80452 | |||
| ddbbcc6f5d | |||
| 0f40afa357 | |||
| cac8d3f405 | |||
| b521a913ab | |||
| d6651ccd82 | |||
| 2976798852 | |||
| 1b90b42647 | |||
| de805e4af7 | |||
| 4d085424f8 | |||
| d07981ea3c | |||
| 22d5cbd0fa | |||
| 4c9ca91b98 | |||
| 0736f325fc | |||
| dcc5afa8fa | |||
| 226465e882 | |||
| 0b62dbe16b | |||
| 2deb4e6f65 | |||
| 9f32f217e6 | |||
| e62781cccb | |||
| e1912fa5be | |||
| a7fe9db773 | |||
| 35296a8692 | |||
| 31801a7312 | |||
| cc7ecd53c7 | |||
| 586e49cab3 | |||
| ebe47a580c | |||
| 7933c10c97 | |||
| 2bc97ee23f | |||
| c88ab5cd12 | |||
| 943aaef76a | |||
| 3fd9e94a34 | |||
| e423f8f605 | |||
| 0eb90ecb3e | |||
| 703f3d0573 | |||
| 2337704602 | |||
| bd9888a439 | |||
| 12b3d0e08d | |||
| 89d350a791 | |||
| 99a83e5522 | |||
| 4d53a26a06 | |||
| a8328cb5ff | |||
| 844c328625 | |||
| 39b97d4e18 | |||
| 45f497e8f6 | |||
| edc5a0412c | |||
| 9670e3a5eb | |||
| 7b2b10bc9e | |||
| bd357adb8b | |||
| 670b6011da | |||
| 150ff72646 | |||
| d567442136 | |||
| 540fff5226 | |||
| 21273c921e | |||
| 75e52427d1 | |||
| 21fd3d0320 | |||
| 1ee798439a | |||
| cc52f980b3 | |||
| ec7e3192bb | |||
| 3fd352a7ef | |||
| 49570c11c6 | |||
| b8ae62a0b1 | |||
| 29a6f0504a | |||
| eadab06f59 | |||
| 9493f2a0eb | |||
| b593b7cc43 | |||
| 5c0c2a17b0 | |||
| 5f33b2fb2d | |||
| 63e0e038fa | |||
| d24a14cb39 | |||
| 3efa52f53b | |||
| 16a4206720 | |||
| e4784d44f6 | |||
| aaf2aac050 | |||
| b5ec0e9744 | |||
| 070a13096b | |||
| 7ccab2bdb9 | |||
| e9434befaa | |||
| 56c77fd5fa | |||
| e6bd88371e | |||
| bc30b01eb7 | |||
| ef93981bfa | |||
| 35e2fc7dd9 | |||
| 2feddf32b2 | |||
| ed185e90f6 | |||
| 19fc66f88a | |||
| 5cbfc7705b | |||
| 8de8c3eb00 | |||
| 04365f12ff | |||
| 9297cd2f3e | |||
| 0711160b1c | |||
| 33f01084d1 | |||
| 1fbec6f601 | |||
| cf5b743de6 | |||
| 2c377e534f | |||
| 1ca9fe35c2 | |||
| 56c49b3b42 | |||
| 5429748767 | |||
| 6aecf59536 | |||
| afc7ff1b7a | |||
| 222e880561 | |||
| 6901e3417b | |||
| cd33562379 | |||
| 073503a6f2 | |||
| 61d5a50534 | |||
| b6e0d4807a | |||
| c7a37c06d8 | |||
| efbd0b9ea0 | |||
| 6ed8f07cb6 | |||
| 5164555c4f | |||
| c5999702ae | |||
| 2a5de8e0f8 | |||
| d05cb31e2e | |||
| c7a9ea2bf4 | |||
| b637ad49d9 | |||
| c6a2c99123 | |||
| 852598fa0a | |||
| 3751d9cf51 | |||
| 30c388aea8 | |||
| b622e3e0a7 | |||
| 086e33dd6e | |||
| 889ce4c4fb | |||
| 96c00271e3 | |||
| a6adedf6e0 | |||
| eb821ceb5c | |||
| 98d61982f9 | |||
| 04a26ada69 | |||
| 3e0dd06374 | |||
| f18828228a | |||
| c812c3192d | |||
| 8d3b919ef6 | |||
| 63df46bf22 | |||
| 826c5e96a1 | |||
| 1870d4905b | |||
| 347b23055d | |||
| cbea5f198f | |||
| be379e188b | |||
| 42d9bd0516 | |||
| 3ca9f351a0 | |||
| a1d6a062a9 |
-19
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
Checks: '
|
|
||||||
bugprone-*,
|
|
||||||
-bugprone-integer-division,
|
|
||||||
-bugprone-narrowing-conversions,
|
|
||||||
performance-*,
|
|
||||||
clang-analyzer-*,
|
|
||||||
misc-*,
|
|
||||||
-misc-unused-parameters,
|
|
||||||
modernize-*,
|
|
||||||
-modernize-avoid-c-arrays,
|
|
||||||
-modernize-deprecated-headers,
|
|
||||||
-modernize-use-auto,
|
|
||||||
-modernize-use-using,
|
|
||||||
-modernize-use-nullptr,
|
|
||||||
-modernize-use-trailing-return-type,
|
|
||||||
'
|
|
||||||
CheckOptions:
|
|
||||||
...
|
|
||||||
@@ -13,27 +13,6 @@
|
|||||||
*.o-*
|
*.o-*
|
||||||
*.os
|
*.os
|
||||||
*.os-*
|
*.os-*
|
||||||
*.so
|
|
||||||
*.a
|
|
||||||
|
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
|
|
||||||
notebooks
|
|
||||||
phone
|
|
||||||
massivemap
|
|
||||||
neos
|
|
||||||
installer
|
|
||||||
chffr/app2
|
|
||||||
chffr/backend/env
|
|
||||||
selfdrive/nav
|
|
||||||
selfdrive/baseui
|
|
||||||
selfdrive/test/simulator2
|
|
||||||
**/cache_data
|
|
||||||
xx/plus
|
|
||||||
xx/community
|
|
||||||
xx/projects
|
|
||||||
!xx/projects/eon_testing_master
|
|
||||||
!xx/projects/map3d
|
|
||||||
xx/ops
|
|
||||||
xx/junk
|
|
||||||
|
|||||||
+3
-1
@@ -7,10 +7,12 @@
|
|||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.otf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
||||||
system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text
|
system/hardware/tici/updater_weston filter=lfs diff=lfs merge=lfs -text
|
||||||
|
system/hardware/tici/updater_magic filter=lfs diff=lfs merge=lfs -text
|
||||||
third_party/**/*.a filter=lfs diff=lfs merge=lfs -text
|
third_party/**/*.a filter=lfs diff=lfs merge=lfs -text
|
||||||
third_party/**/*.so filter=lfs diff=lfs merge=lfs -text
|
third_party/**/*.so filter=lfs diff=lfs merge=lfs -text
|
||||||
third_party/**/*.so.* filter=lfs diff=lfs merge=lfs -text
|
third_party/**/*.so.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ simulation:
|
|||||||
|
|
||||||
ui:
|
ui:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-all-files: '{selfdrive/ui/**,system/ui/**}'
|
- any-glob-to-all-files: '{selfdrive/assets/**,selfdrive/ui/**,system/ui/**}'
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- uses: ./.github/workflows/setup-with-retry
|
- uses: ./.github/workflows/setup-with-retry
|
||||||
- name: Push badges
|
- name: Push badges
|
||||||
run: |
|
run: |
|
||||||
${{ env.RUN }} "scons -j$(nproc) && python3 selfdrive/ui/translations/create_badges.py"
|
${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py"
|
||||||
|
|
||||||
rm .gitattributes
|
rm .gitattributes
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||||
cd gitlab_docs
|
cd gitlab_docs
|
||||||
git checkout main
|
git checkout main
|
||||||
git sparse-checkout set --no-cone models/
|
git sparse-checkout set --no-cone models/
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||||
run: |
|
run: |
|
||||||
echo "Cloning GitLab"
|
echo "Cloning GitLab"
|
||||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||||
cd gitlab_docs
|
cd gitlab_docs
|
||||||
echo "checkout models/${RECOMPILED_DIR}"
|
echo "checkout models/${RECOMPILED_DIR}"
|
||||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||||
run: |
|
run: |
|
||||||
echo "Cloning GitLab"
|
echo "Cloning GitLab"
|
||||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||||
cd gitlab_docs
|
cd gitlab_docs
|
||||||
echo "checkout models/${RECOMPILED_DIR}"
|
echo "checkout models/${RECOMPILED_DIR}"
|
||||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
selfdrive_tests:
|
tests:
|
||||||
uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master
|
uses: sunnypilot/sunnypilot/.github/workflows/tests.yaml@master
|
||||||
with:
|
with:
|
||||||
run_number: ${{ inputs.run_number }}
|
run_number: ${{ inputs.run_number }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: "ui preview"
|
name: "raylib ui preview"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -8,14 +8,16 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
paths:
|
paths:
|
||||||
|
- 'selfdrive/assets/**'
|
||||||
- 'selfdrive/ui/**'
|
- 'selfdrive/ui/**'
|
||||||
|
- 'system/ui/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
UI_JOB_NAME: "Create UI Report"
|
UI_JOB_NAME: "Create raylib UI Report"
|
||||||
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||||
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
|
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
|
||||||
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}"
|
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
preview:
|
preview:
|
||||||
@@ -52,7 +54,7 @@ jobs:
|
|||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run_id: ${{ steps.get_run_id.outputs.run_id }}
|
run_id: ${{ steps.get_run_id.outputs.run_id }}
|
||||||
search_artifacts: true
|
search_artifacts: true
|
||||||
name: report-1-${{ env.REPORT_NAME }}
|
name: raylib-report-1-${{ env.REPORT_NAME }}
|
||||||
path: ${{ github.workspace }}/pr_ui
|
path: ${{ github.workspace }}/pr_ui
|
||||||
|
|
||||||
- name: Getting master ui
|
- name: Getting master ui
|
||||||
@@ -60,23 +62,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repository: sunnypilot/ci-artifacts
|
repository: sunnypilot/ci-artifacts
|
||||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
||||||
path: ${{ github.workspace }}/master_ui
|
path: ${{ github.workspace }}/master_ui_raylib
|
||||||
ref: openpilot_master_ui
|
ref: openpilot_master_ui_raylib
|
||||||
|
|
||||||
- name: Saving new master ui
|
- name: Saving new master ui
|
||||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||||
working-directory: ${{ github.workspace }}/master_ui
|
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||||
run: |
|
run: |
|
||||||
git checkout --orphan=new_master_ui
|
git checkout --orphan=new_master_ui_raylib
|
||||||
git rm -rf *
|
git rm -rf *
|
||||||
git branch -D openpilot_master_ui
|
git branch -D openpilot_master_ui_raylib
|
||||||
git branch -m openpilot_master_ui
|
git branch -m openpilot_master_ui_raylib
|
||||||
git config user.name "GitHub Actions Bot"
|
git config user.name "GitHub Actions Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
mv ${{ github.workspace }}/pr_ui/*.png .
|
mv ${{ github.workspace }}/pr_ui/*.png .
|
||||||
git add .
|
git add .
|
||||||
git commit -m "screenshots for commit ${{ env.SHA }}"
|
git commit -m "raylib screenshots for commit ${{ env.SHA }}"
|
||||||
git push origin openpilot_master_ui --force
|
git push origin openpilot_master_ui_raylib --force
|
||||||
|
|
||||||
- name: Finding diff
|
- name: Finding diff
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
@@ -94,7 +96,7 @@ jobs:
|
|||||||
for ((i=0; i<${#A[*]}; i=i+1));
|
for ((i=0; i<${#A[*]}; i=i+1));
|
||||||
do
|
do
|
||||||
# Check if the master file exists
|
# Check if the master file exists
|
||||||
if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then
|
if [ ! -f "${{ github.workspace }}/master_ui_raylib/${A[$i]}.png" ]; then
|
||||||
# This is a new file in PR UI that doesn't exist in master
|
# This is a new file in PR UI that doesn't exist in master
|
||||||
DIFF="${DIFF}<details open>"
|
DIFF="${DIFF}<details open>"
|
||||||
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
|
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
|
||||||
@@ -106,12 +108,12 @@ jobs:
|
|||||||
|
|
||||||
DIFF="${DIFF}</table>"
|
DIFF="${DIFF}</table>"
|
||||||
DIFF="${DIFF}</details>"
|
DIFF="${DIFF}</details>"
|
||||||
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
|
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
|
||||||
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png
|
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png
|
||||||
composite mask.png ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png
|
composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png
|
||||||
convert -delay 100 ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
|
convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
|
||||||
|
|
||||||
mv ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png
|
mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png
|
||||||
|
|
||||||
DIFF="${DIFF}<details open>"
|
DIFF="${DIFF}<details open>"
|
||||||
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>"
|
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>"
|
||||||
@@ -149,7 +151,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Saving proposed ui
|
- name: Saving proposed ui
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
working-directory: ${{ github.workspace }}/master_ui
|
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||||
run: |
|
run: |
|
||||||
git config user.name "GitHub Actions Bot"
|
git config user.name "GitHub Actions Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
@@ -157,7 +159,7 @@ jobs:
|
|||||||
git rm -rf *
|
git rm -rf *
|
||||||
mv ${{ github.workspace }}/pr_ui/* .
|
mv ${{ github.workspace }}/pr_ui/* .
|
||||||
git add .
|
git add .
|
||||||
git commit -m "screenshots for PR #${{ github.event.number }}"
|
git commit -m "raylib screenshots for PR #${{ github.event.number }}"
|
||||||
git push origin ${{ env.BRANCH_NAME }} --force
|
git push origin ${{ env.BRANCH_NAME }} --force
|
||||||
|
|
||||||
- name: Comment Screenshots on PR
|
- name: Comment Screenshots on PR
|
||||||
@@ -165,9 +167,9 @@ jobs:
|
|||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
with:
|
with:
|
||||||
message: |
|
message: |
|
||||||
<!-- _(run_id_screenshots **${{ github.run_id }}**)_ -->
|
<!-- _(run_id_screenshots_raylib **${{ github.run_id }}**)_ -->
|
||||||
## UI Preview
|
## raylib UI Preview
|
||||||
${{ steps.find_diff.outputs.DIFF }}
|
${{ steps.find_diff.outputs.DIFF }}
|
||||||
comment_tag: run_id_screenshots
|
comment_tag: run_id_screenshots_raylib
|
||||||
pr_number: ${{ github.event.number }}
|
pr_number: ${{ github.event.number }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -156,6 +156,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||||
path: ${{ github.workspace }}/selfdrive/modeld/models
|
path: ${{ github.workspace }}/selfdrive/modeld/models
|
||||||
|
- run: |
|
||||||
|
rm -f ${{ github.workspace }}/selfdrive/modeld/models/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx
|
||||||
|
|
||||||
- name: Build Model
|
- name: Build Model
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: selfdrive
|
name: tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -14,18 +14,19 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
|
group: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHONWARNINGS: error
|
PYTHONWARNINGS: error
|
||||||
BASE_IMAGE: sunnypilot-base
|
BASE_IMAGE: sunnypilot-base
|
||||||
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
||||||
|
MAPBOX_TOKEN_CI: ${{ secrets.MAPBOX_TOKEN_CI }}
|
||||||
|
|
||||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||||
BUILD: release/ci/docker_build_sp.sh base
|
BUILD: release/ci/docker_build_sp.sh base
|
||||||
|
|
||||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -e MAPBOX_TOKEN_CI=$MAPBOX_TOKEN_CI -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||||
|
|
||||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||||
|
|
||||||
@@ -195,8 +196,6 @@ jobs:
|
|||||||
# Pre-compile Python bytecode so each pytest worker doesn't need to
|
# Pre-compile Python bytecode so each pytest worker doesn't need to
|
||||||
$PYTEST --collect-only -m 'not slow' -qq && \
|
$PYTEST --collect-only -m 'not slow' -qq && \
|
||||||
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
|
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
|
||||||
./selfdrive/ui/tests/create_test_translations.sh && \
|
|
||||||
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
|
|
||||||
chmod -R 777 /tmp/comma_download_cache"
|
chmod -R 777 /tmp/comma_download_cache"
|
||||||
|
|
||||||
process_replay:
|
process_replay:
|
||||||
@@ -257,7 +256,7 @@ jobs:
|
|||||||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
||||||
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|
||||||
|| fromJSON('["ubuntu-24.04"]') }}
|
|| fromJSON('["ubuntu-24.04"]') }}
|
||||||
if: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
if: false # FIXME: Started to timeout recently
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -274,38 +273,28 @@ jobs:
|
|||||||
source selfdrive/test/setup_vsound.sh && \
|
source selfdrive/test/setup_vsound.sh && \
|
||||||
CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py"
|
CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py"
|
||||||
|
|
||||||
create_ui_report:
|
create_raylib_ui_report:
|
||||||
# This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml
|
name: Create raylib UI Report
|
||||||
name: Create UI Report
|
|
||||||
runs-on: ${{
|
runs-on: ${{
|
||||||
(github.repository == 'commaai/openpilot') &&
|
(github.repository == 'commaai/openpilot') &&
|
||||||
((github.event_name != 'pull_request') ||
|
((github.event_name != 'pull_request') ||
|
||||||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
||||||
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|
||||||
|| fromJSON('["ubuntu-24.04"]') }}
|
|| fromJSON('["ubuntu-24.04"]') }}
|
||||||
if: false # FIXME: FrameReader is broken on CI runners
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- uses: ./.github/workflows/setup-with-retry
|
- uses: ./.github/workflows/setup-with-retry
|
||||||
- name: caching frames
|
|
||||||
id: frames-cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: .ci_cache/comma_download_cache
|
|
||||||
key: ui_screenshots_test_${{ hashFiles('selfdrive/ui/tests/test_ui/run.py') }}
|
|
||||||
- name: Build openpilot
|
- name: Build openpilot
|
||||||
run: ${{ env.RUN }} "scons -j$(nproc)"
|
run: ${{ env.RUN }} "scons -j$(nproc)"
|
||||||
- name: Create Test Report
|
- name: Create raylib UI Report
|
||||||
timeout-minutes: ${{ ((steps.frames-cache.outputs.cache-hit == 'true') && 2 || 4) }}
|
|
||||||
run: >
|
run: >
|
||||||
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
|
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
|
||||||
source selfdrive/test/setup_xvfb.sh &&
|
source selfdrive/test/setup_xvfb.sh &&
|
||||||
CACHE_ROOT=/tmp/comma_download_cache python3 selfdrive/ui/tests/test_ui/run.py &&
|
python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py"
|
||||||
chmod -R 777 /tmp/comma_download_cache"
|
- name: Upload Raylib UI Report
|
||||||
- name: Upload Test Report
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||||
path: selfdrive/ui/tests/test_ui/report_1/screenshots
|
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots
|
||||||
+3
-9
@@ -10,7 +10,6 @@ venv/
|
|||||||
.overlay_init
|
.overlay_init
|
||||||
.overlay_consistent
|
.overlay_consistent
|
||||||
.sconsign.dblite
|
.sconsign.dblite
|
||||||
model2.png
|
|
||||||
a.out
|
a.out
|
||||||
.hypothesis
|
.hypothesis
|
||||||
.cache/
|
.cache/
|
||||||
@@ -37,29 +36,23 @@ a.out
|
|||||||
*.class
|
*.class
|
||||||
*.pyxbldc
|
*.pyxbldc
|
||||||
*.vcd
|
*.vcd
|
||||||
*.qm
|
*.mo
|
||||||
*_pyx.cpp
|
*_pyx.cpp
|
||||||
|
*.stats
|
||||||
config.json
|
config.json
|
||||||
clcache
|
clcache
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
compare_runtime*.html
|
compare_runtime*.html
|
||||||
|
|
||||||
persist
|
|
||||||
selfdrive/pandad/pandad
|
selfdrive/pandad/pandad
|
||||||
cereal/services.h
|
cereal/services.h
|
||||||
cereal/gen
|
cereal/gen
|
||||||
cereal/messaging/bridge
|
cereal/messaging/bridge
|
||||||
selfdrive/mapd/default_speeds_by_region.json
|
|
||||||
selfdrive/ui/translations/tmp
|
selfdrive/ui/translations/tmp
|
||||||
selfdrive/test/longitudinal_maneuvers/out
|
|
||||||
selfdrive/car/tests/cars_dump
|
selfdrive/car/tests/cars_dump
|
||||||
system/camerad/camerad
|
system/camerad/camerad
|
||||||
system/camerad/test/ae_gray_test
|
system/camerad/test/ae_gray_test
|
||||||
|
|
||||||
notebooks
|
|
||||||
hyperthneed
|
|
||||||
provisioning
|
|
||||||
|
|
||||||
.coverage*
|
.coverage*
|
||||||
coverage.xml
|
coverage.xml
|
||||||
htmlcov
|
htmlcov
|
||||||
@@ -76,6 +69,7 @@ sunnypilot/modeld*/thneed/compile
|
|||||||
sunnypilot/modeld*/models/*.thneed
|
sunnypilot/modeld*/models/*.thneed
|
||||||
sunnypilot/modeld*/models/*.pkl
|
sunnypilot/modeld*/models/*.pkl
|
||||||
|
|
||||||
|
# openpilot log files
|
||||||
*.bz2
|
*.bz2
|
||||||
*.zst
|
*.zst
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
url = https://github.com/commaai/teleoprtc
|
url = https://github.com/commaai/teleoprtc
|
||||||
[submodule "tinygrad"]
|
[submodule "tinygrad"]
|
||||||
path = tinygrad_repo
|
path = tinygrad_repo
|
||||||
url = https://github.com/tinygrad/tinygrad.git
|
url = https://github.com/commaai/tinygrad.git
|
||||||
[submodule "sunnypilot/neural_network_data"]
|
[submodule "sunnypilot/neural_network_data"]
|
||||||
path = sunnypilot/neural_network_data
|
path = sunnypilot/neural_network_data
|
||||||
url = https://github.com/sunnypilot/neural-network-data.git
|
url = https://github.com/sunnypilot/neural-network-data.git
|
||||||
|
|||||||
+25
-1
@@ -1,6 +1,30 @@
|
|||||||
sunnypilot Version 2025.002.000 (2025-xx-xx)
|
sunnypilot Version 2025.003.000 (20xx-xx-xx)
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
sunnypilot Version 2025.002.000 (2025-11-06)
|
||||||
|
========================
|
||||||
|
* What's Changed (sunnypilot/sunnypilot)
|
||||||
|
* models: bump model json to v8 by @Discountchubbs
|
||||||
|
* Bug: Model UI Crash Fix by @nayan8teen
|
||||||
|
* controlsd: add `CP_SP` to `get_pid_accel_limits` by @THERoenPR
|
||||||
|
* sunnylink: update uploader button logic to support novice tier and above by @devtekve
|
||||||
|
* Tesla: Coop Steering by @AmyJeanes
|
||||||
|
* ui: update discord references and add forum widget by @devtekve
|
||||||
|
* ui: Fix spacing in sunnylink panel by @devtekve
|
||||||
|
* docs: Update README installation branches and discord links by @mpurnell1 in
|
||||||
|
* stats: sunnylink integration by @devtekve
|
||||||
|
* bug: Fix initial registration for sunnylink by @devtekve
|
||||||
|
* What's Changed (sunnypilot/opendbc)
|
||||||
|
* Honda: add brake hold messages for Clarity by @mvl-boston
|
||||||
|
* interface: add `CP_SP` to `get_pid_accel_limits` method signature by @roenthomas
|
||||||
|
* Honda: use fixed accel min/max constants for Gas Interceptor by @roenthomas
|
||||||
|
* Tesla: Coop Steering by @AmyJeanes
|
||||||
|
* New Contributors (sunnypilot/sunnypilot)
|
||||||
|
* @THERoenPR made their first contribution in "controlsd: add `CP_SP` to `get_pid_accel_limits`"
|
||||||
|
* @AmyJeanes made their first contribution in "Tesla: Coop Steering"
|
||||||
|
* @mpurnell1 made their first contribution in "docs: Update README installation branches and discord links"
|
||||||
|
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.001.000...v2025.002.000
|
||||||
|
|
||||||
sunnypilot Version 2025.001.000 (2025-10-25)
|
sunnypilot Version 2025.001.000 (2025-10-25)
|
||||||
========================
|
========================
|
||||||
* 🛠️ Major rewrite
|
* 🛠️ Major rewrite
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ WORKDIR ${OPENPILOT_PATH}
|
|||||||
|
|
||||||
COPY . ${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)
|
||||||
|
|||||||
Vendored
+3
-3
@@ -167,7 +167,7 @@ node {
|
|||||||
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
|
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
|
||||||
|
|
||||||
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
|
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
|
||||||
'release-tici', 'testing-closet*', 'hotfix-*']
|
'release-tici', 'release-tizi', 'release-tizi-staging', 'testing-closet*', 'hotfix-*']
|
||||||
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
|
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
|
||||||
|
|
||||||
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {
|
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {
|
||||||
@@ -178,8 +178,8 @@ node {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (env.BRANCH_NAME == 'devel-staging') {
|
if (env.BRANCH_NAME == 'devel-staging') {
|
||||||
deviceStage("build release3-staging", "tizi-needs-can", [], [
|
deviceStage("build release-tizi-staging", "tizi-needs-can", [], [
|
||||||
step("build release3-staging", "RELEASE_BRANCH=release3-staging $SOURCE_DIR/release/build_release.sh"),
|
step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging $SOURCE_DIR/release/build_release.sh"),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -1,13 +1,20 @@
|
|||||||
|
Version 0.10.2 (2025-11-23)
|
||||||
|
========================
|
||||||
|
|
||||||
Version 0.10.1 (2025-09-08)
|
Version 0.10.1 (2025-09-08)
|
||||||
========================
|
========================
|
||||||
* New driving model
|
* New driving model #36276
|
||||||
* World Model: removed global localization inputs
|
* World Model: removed global localization inputs
|
||||||
* World Model: 2x the number of parameters
|
* World Model: 2x the number of parameters
|
||||||
* World Model: trained on 4x the number of segments
|
* World Model: trained on 4x the number of segments
|
||||||
|
* VAE Compression Model: new architecture and training objective
|
||||||
* Driving Vision Model: trained on 4x the number of segments
|
* Driving Vision Model: trained on 4x the number of segments
|
||||||
|
* New Driver Monitoring model #36198
|
||||||
|
* Acura TLX 2021 support thanks to MVL!
|
||||||
* Honda City 2023 support thanks to vanillagorillaa and drFritz!
|
* Honda City 2023 support thanks to vanillagorillaa and drFritz!
|
||||||
* Honda N-Box 2018 support thanks to miettal!
|
* Honda N-Box 2018 support thanks to miettal!
|
||||||
* Honda Odyssey 2021-25 support thanks to csouers and MVL!
|
* Honda Odyssey 2021-25 support thanks to csouers and MVL!
|
||||||
|
* Honda Passport 2026 support thanks to vanillagorillaa and MVL!
|
||||||
|
|
||||||
Version 0.10.0 (2025-08-05)
|
Version 0.10.0 (2025-08-05)
|
||||||
========================
|
========================
|
||||||
|
|||||||
+103
-248
@@ -3,176 +3,52 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import platform
|
import platform
|
||||||
|
import shlex
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import SCons.Errors
|
import SCons.Errors
|
||||||
|
|
||||||
SCons.Warnings.warningAsException(True)
|
SCons.Warnings.warningAsException(True)
|
||||||
|
|
||||||
# pending upstream fix - https://github.com/SCons/scons/issues/4461
|
|
||||||
#SetOption('warn', 'all')
|
|
||||||
|
|
||||||
TICI = os.path.isfile('/TICI')
|
|
||||||
AGNOS = TICI
|
|
||||||
|
|
||||||
Decider('MD5-timestamp')
|
Decider('MD5-timestamp')
|
||||||
|
|
||||||
SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
|
SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
|
||||||
|
|
||||||
AddOption('--kaitai',
|
AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers')
|
||||||
action='store_true',
|
AddOption('--asan', action='store_true', help='turn on ASAN')
|
||||||
help='Regenerate kaitai struct parsers')
|
AddOption('--ubsan', action='store_true', help='turn on UBSan')
|
||||||
|
AddOption('--mutation', action='store_true', help='generate mutation-ready code')
|
||||||
AddOption('--asan',
|
AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line')
|
||||||
action='store_true',
|
|
||||||
help='turn on ASAN')
|
|
||||||
|
|
||||||
AddOption('--ubsan',
|
|
||||||
action='store_true',
|
|
||||||
help='turn on UBSan')
|
|
||||||
|
|
||||||
AddOption('--coverage',
|
|
||||||
action='store_true',
|
|
||||||
help='build with test coverage options')
|
|
||||||
|
|
||||||
AddOption('--clazy',
|
|
||||||
action='store_true',
|
|
||||||
help='build with clazy')
|
|
||||||
|
|
||||||
AddOption('--ccflags',
|
|
||||||
action='store',
|
|
||||||
type='string',
|
|
||||||
default='',
|
|
||||||
help='pass arbitrary flags over the command line')
|
|
||||||
|
|
||||||
AddOption('--external-sconscript',
|
|
||||||
action='store',
|
|
||||||
metavar='FILE',
|
|
||||||
dest='external_sconscript',
|
|
||||||
help='add an external SConscript to the build')
|
|
||||||
|
|
||||||
AddOption('--mutation',
|
|
||||||
action='store_true',
|
|
||||||
help='generate mutation-ready code')
|
|
||||||
|
|
||||||
AddOption('--minimal',
|
AddOption('--minimal',
|
||||||
action='store_false',
|
action='store_false',
|
||||||
dest='extras',
|
dest='extras',
|
||||||
default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS)
|
default=os.path.exists(File('#.gitattributes').abspath), # minimal by default on release branch (where there's no LFS)
|
||||||
help='the minimum build to run openpilot. no tests, tools, etc.')
|
help='the minimum build to run openpilot. no tests, tools, etc.')
|
||||||
|
|
||||||
AddOption('--stock-ui',
|
# Detect platform
|
||||||
action='store_true',
|
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
|
||||||
dest='stock_ui',
|
|
||||||
default=False,
|
|
||||||
help='Build stock openpilot UI instead of sunnypilot UI')
|
|
||||||
|
|
||||||
## Architecture name breakdown (arch)
|
|
||||||
## - larch64: linux tici aarch64
|
|
||||||
## - aarch64: linux pc aarch64
|
|
||||||
## - x86_64: linux pc x64
|
|
||||||
## - Darwin: mac x64 or arm64
|
|
||||||
real_arch = arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
|
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
arch = "Darwin"
|
arch = "Darwin"
|
||||||
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
|
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
|
||||||
elif arch == "aarch64" and AGNOS:
|
elif arch == "aarch64" and os.path.isfile('/TICI'):
|
||||||
arch = "larch64"
|
arch = "larch64"
|
||||||
assert arch in ["larch64", "aarch64", "x86_64", "Darwin"]
|
assert arch in [
|
||||||
|
"larch64", # linux tici arm64
|
||||||
lenv = {
|
"aarch64", # linux pc arm64
|
||||||
"PATH": os.environ['PATH'],
|
"x86_64", # linux pc x64
|
||||||
"PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath,
|
"Darwin", # macOS arm64 (x86 not supported)
|
||||||
|
]
|
||||||
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
|
|
||||||
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
|
|
||||||
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
|
|
||||||
}
|
|
||||||
|
|
||||||
rpath = []
|
|
||||||
|
|
||||||
if arch == "larch64":
|
|
||||||
cpppath = [
|
|
||||||
"#third_party/opencl/include",
|
|
||||||
]
|
|
||||||
|
|
||||||
libpath = [
|
|
||||||
"/usr/local/lib",
|
|
||||||
"/system/vendor/lib64",
|
|
||||||
f"#third_party/acados/{arch}/lib",
|
|
||||||
]
|
|
||||||
|
|
||||||
libpath += [
|
|
||||||
"#third_party/snpe/larch64",
|
|
||||||
"#third_party/libyuv/larch64/lib",
|
|
||||||
"/usr/lib/aarch64-linux-gnu"
|
|
||||||
]
|
|
||||||
cflags = ["-DQCOM2", "-mcpu=cortex-a57"]
|
|
||||||
cxxflags = ["-DQCOM2", "-mcpu=cortex-a57"]
|
|
||||||
rpath += ["/usr/local/lib"]
|
|
||||||
else:
|
|
||||||
cflags = []
|
|
||||||
cxxflags = []
|
|
||||||
cpppath = []
|
|
||||||
rpath += []
|
|
||||||
|
|
||||||
# MacOS
|
|
||||||
if arch == "Darwin":
|
|
||||||
libpath = [
|
|
||||||
f"#third_party/libyuv/{arch}/lib",
|
|
||||||
f"#third_party/acados/{arch}/lib",
|
|
||||||
f"{brew_prefix}/lib",
|
|
||||||
f"{brew_prefix}/opt/openssl@3.0/lib",
|
|
||||||
"/System/Library/Frameworks/OpenGL.framework/Libraries",
|
|
||||||
]
|
|
||||||
|
|
||||||
cflags += ["-DGL_SILENCE_DEPRECATION"]
|
|
||||||
cxxflags += ["-DGL_SILENCE_DEPRECATION"]
|
|
||||||
cpppath += [
|
|
||||||
f"{brew_prefix}/include",
|
|
||||||
f"{brew_prefix}/opt/openssl@3.0/include",
|
|
||||||
]
|
|
||||||
# Linux
|
|
||||||
else:
|
|
||||||
libpath = [
|
|
||||||
f"#third_party/acados/{arch}/lib",
|
|
||||||
f"#third_party/libyuv/{arch}/lib",
|
|
||||||
"/usr/lib",
|
|
||||||
"/usr/local/lib",
|
|
||||||
]
|
|
||||||
|
|
||||||
if arch == "x86_64":
|
|
||||||
libpath += [
|
|
||||||
f"#third_party/snpe/{arch}"
|
|
||||||
]
|
|
||||||
rpath += [
|
|
||||||
Dir(f"#third_party/snpe/{arch}").abspath,
|
|
||||||
]
|
|
||||||
|
|
||||||
if GetOption('asan'):
|
|
||||||
ccflags = ["-fsanitize=address", "-fno-omit-frame-pointer"]
|
|
||||||
ldflags = ["-fsanitize=address"]
|
|
||||||
elif GetOption('ubsan'):
|
|
||||||
ccflags = ["-fsanitize=undefined"]
|
|
||||||
ldflags = ["-fsanitize=undefined"]
|
|
||||||
else:
|
|
||||||
ccflags = []
|
|
||||||
ldflags = []
|
|
||||||
|
|
||||||
# no --as-needed on mac linker
|
|
||||||
if arch != "Darwin":
|
|
||||||
ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"]
|
|
||||||
|
|
||||||
if not GetOption('stock_ui'):
|
|
||||||
cflags += ["-DSUNNYPILOT"]
|
|
||||||
cxxflags += ["-DSUNNYPILOT"]
|
|
||||||
|
|
||||||
ccflags_option = GetOption('ccflags')
|
|
||||||
if ccflags_option:
|
|
||||||
ccflags += ccflags_option.split(' ')
|
|
||||||
|
|
||||||
env = Environment(
|
env = Environment(
|
||||||
ENV=lenv,
|
ENV={
|
||||||
|
"PATH": os.environ['PATH'],
|
||||||
|
"PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath,
|
||||||
|
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
|
||||||
|
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
|
||||||
|
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
|
||||||
|
},
|
||||||
|
CC='clang',
|
||||||
|
CXX='clang++',
|
||||||
CCFLAGS=[
|
CCFLAGS=[
|
||||||
"-g",
|
"-g",
|
||||||
"-fPIC",
|
"-fPIC",
|
||||||
@@ -185,37 +61,32 @@ env = Environment(
|
|||||||
"-Wno-c99-designator",
|
"-Wno-c99-designator",
|
||||||
"-Wno-reorder-init-list",
|
"-Wno-reorder-init-list",
|
||||||
"-Wno-vla-cxx-extension",
|
"-Wno-vla-cxx-extension",
|
||||||
] + cflags + ccflags,
|
],
|
||||||
|
CFLAGS=["-std=gnu11"],
|
||||||
CPPPATH=cpppath + [
|
CXXFLAGS=["-std=c++1z"],
|
||||||
|
CPPPATH=[
|
||||||
"#",
|
"#",
|
||||||
|
"#msgq",
|
||||||
|
"#third_party",
|
||||||
|
"#third_party/json11",
|
||||||
|
"#third_party/linux/include",
|
||||||
"#third_party/acados/include",
|
"#third_party/acados/include",
|
||||||
"#third_party/acados/include/blasfeo/include",
|
"#third_party/acados/include/blasfeo/include",
|
||||||
"#third_party/acados/include/hpipm/include",
|
"#third_party/acados/include/hpipm/include",
|
||||||
"#third_party/catch2/include",
|
"#third_party/catch2/include",
|
||||||
"#third_party/libyuv/include",
|
"#third_party/libyuv/include",
|
||||||
"#third_party/json11",
|
|
||||||
"#third_party/linux/include",
|
|
||||||
"#third_party/snpe/include",
|
"#third_party/snpe/include",
|
||||||
"#third_party",
|
|
||||||
"#msgq",
|
|
||||||
],
|
],
|
||||||
|
LIBPATH=[
|
||||||
CC='clang',
|
"#common",
|
||||||
CXX='clang++',
|
|
||||||
LINKFLAGS=ldflags,
|
|
||||||
|
|
||||||
RPATH=rpath,
|
|
||||||
|
|
||||||
CFLAGS=["-std=gnu11"] + cflags,
|
|
||||||
CXXFLAGS=["-std=c++1z"] + cxxflags,
|
|
||||||
LIBPATH=libpath + [
|
|
||||||
"#msgq_repo",
|
"#msgq_repo",
|
||||||
"#third_party",
|
"#third_party",
|
||||||
"#selfdrive/pandad",
|
"#selfdrive/pandad",
|
||||||
"#common",
|
|
||||||
"#rednose/helpers",
|
"#rednose/helpers",
|
||||||
|
f"#third_party/libyuv/{arch}/lib",
|
||||||
|
f"#third_party/acados/{arch}/lib",
|
||||||
],
|
],
|
||||||
|
RPATH=[],
|
||||||
CYTHONCFILESUFFIX=".cpp",
|
CYTHONCFILESUFFIX=".cpp",
|
||||||
COMPILATIONDB_USE_ABSPATH=True,
|
COMPILATIONDB_USE_ABSPATH=True,
|
||||||
REDNOSE_ROOT="#",
|
REDNOSE_ROOT="#",
|
||||||
@@ -223,30 +94,72 @@ env = Environment(
|
|||||||
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if arch == "Darwin":
|
# Arch-specific flags and paths
|
||||||
# RPATH is not supported on macOS, instead use the linker flags
|
if arch == "larch64":
|
||||||
darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]]
|
env.Append(CPPPATH=["#third_party/opencl/include"])
|
||||||
env["LINKFLAGS"] += darwin_rpath_link_flags
|
env.Append(LIBPATH=[
|
||||||
|
"/usr/local/lib",
|
||||||
|
"/system/vendor/lib64",
|
||||||
|
"/usr/lib/aarch64-linux-gnu",
|
||||||
|
"#third_party/snpe/larch64",
|
||||||
|
])
|
||||||
|
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
|
||||||
|
env.Append(CCFLAGS=arch_flags)
|
||||||
|
env.Append(CXXFLAGS=arch_flags)
|
||||||
|
elif arch == "Darwin":
|
||||||
|
env.Append(LIBPATH=[
|
||||||
|
f"{brew_prefix}/lib",
|
||||||
|
f"{brew_prefix}/opt/openssl@3.0/lib",
|
||||||
|
f"{brew_prefix}/opt/llvm/lib/c++",
|
||||||
|
"/System/Library/Frameworks/OpenGL.framework/Libraries",
|
||||||
|
])
|
||||||
|
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
|
||||||
|
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
|
||||||
|
env.Append(CPPPATH=[
|
||||||
|
f"{brew_prefix}/include",
|
||||||
|
f"{brew_prefix}/opt/openssl@3.0/include",
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
env.Append(LIBPATH=[
|
||||||
|
"/usr/lib",
|
||||||
|
"/usr/local/lib",
|
||||||
|
])
|
||||||
|
|
||||||
env.CompilationDatabase('compile_commands.json')
|
if arch == "x86_64":
|
||||||
|
env.Append(LIBPATH=[
|
||||||
|
f"#third_party/snpe/{arch}"
|
||||||
|
])
|
||||||
|
env.Append(RPATH=[
|
||||||
|
Dir(f"#third_party/snpe/{arch}").abspath,
|
||||||
|
])
|
||||||
|
|
||||||
# Setup cache dir
|
# Sanitizers and extra CCFLAGS from CLI
|
||||||
default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache'
|
if GetOption('asan'):
|
||||||
cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir)
|
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
|
||||||
CacheDir(cache_dir)
|
env.Append(LINKFLAGS=["-fsanitize=address"])
|
||||||
Clean(["."], cache_dir)
|
elif GetOption('ubsan'):
|
||||||
|
env.Append(CCFLAGS=["-fsanitize=undefined"])
|
||||||
|
env.Append(LINKFLAGS=["-fsanitize=undefined"])
|
||||||
|
|
||||||
|
_extra_cc = shlex.split(GetOption('ccflags') or '')
|
||||||
|
if _extra_cc:
|
||||||
|
env.Append(CCFLAGS=_extra_cc)
|
||||||
|
|
||||||
|
# no --as-needed on mac linker
|
||||||
|
if arch != "Darwin":
|
||||||
|
env.Append(LINKFLAGS=["-Wl,--as-needed", "-Wl,--no-undefined"])
|
||||||
|
|
||||||
|
# progress output
|
||||||
node_interval = 5
|
node_interval = 5
|
||||||
node_count = 0
|
node_count = 0
|
||||||
def progress_function(node):
|
def progress_function(node):
|
||||||
global node_count
|
global node_count
|
||||||
node_count += node_interval
|
node_count += node_interval
|
||||||
sys.stderr.write("progress: %d\n" % node_count)
|
sys.stderr.write("progress: %d\n" % node_count)
|
||||||
|
|
||||||
if os.environ.get('SCONS_PROGRESS'):
|
if os.environ.get('SCONS_PROGRESS'):
|
||||||
Progress(progress_function, interval=node_interval)
|
Progress(progress_function, interval=node_interval)
|
||||||
|
|
||||||
# Cython build environment
|
# ********** Cython build environment **********
|
||||||
py_include = sysconfig.get_paths()['include']
|
py_include = sysconfig.get_paths()['include']
|
||||||
envCython = env.Clone()
|
envCython = env.Clone()
|
||||||
envCython["CPPPATH"] += [py_include, np.get_include()]
|
envCython["CPPPATH"] += [py_include, np.get_include()]
|
||||||
@@ -255,84 +168,27 @@ envCython["CCFLAGS"].remove("-Werror")
|
|||||||
|
|
||||||
envCython["LIBS"] = []
|
envCython["LIBS"] = []
|
||||||
if arch == "Darwin":
|
if arch == "Darwin":
|
||||||
envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"] + darwin_rpath_link_flags
|
envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"]
|
||||||
else:
|
else:
|
||||||
envCython["LINKFLAGS"] = ["-pthread", "-shared"]
|
envCython["LINKFLAGS"] = ["-pthread", "-shared"]
|
||||||
|
|
||||||
np_version = SCons.Script.Value(np.__version__)
|
np_version = SCons.Script.Value(np.__version__)
|
||||||
Export('envCython', 'np_version')
|
Export('envCython', 'np_version')
|
||||||
|
|
||||||
# Qt build environment
|
Export('env', 'arch')
|
||||||
qt_env = env.Clone()
|
|
||||||
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"]
|
|
||||||
|
|
||||||
qt_libs = []
|
# Setup cache dir
|
||||||
if arch == "Darwin":
|
cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
|
||||||
qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5"
|
CacheDir(cache_dir)
|
||||||
qt_dirs = [
|
Clean(["."], cache_dir)
|
||||||
os.path.join(qt_env['QTDIR'], "include"),
|
|
||||||
]
|
|
||||||
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
|
|
||||||
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
|
|
||||||
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"]
|
|
||||||
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
|
|
||||||
else:
|
|
||||||
qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
|
|
||||||
qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip()
|
|
||||||
|
|
||||||
qt_env['QTDIR'] = qt_install_prefix
|
# ********** start building stuff **********
|
||||||
qt_dirs = [
|
|
||||||
f"{qt_install_headers}",
|
|
||||||
]
|
|
||||||
|
|
||||||
qt_gui_path = os.path.join(qt_install_headers, "QtGui")
|
|
||||||
qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))]
|
|
||||||
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
|
|
||||||
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
|
|
||||||
|
|
||||||
qt_libs = [f"Qt5{m}" for m in qt_modules]
|
|
||||||
if arch == "larch64":
|
|
||||||
qt_libs += ["GLESv2", "wayland-client"]
|
|
||||||
qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath)
|
|
||||||
elif arch != "Darwin":
|
|
||||||
qt_libs += ["GL"]
|
|
||||||
qt_env['QT3DIR'] = qt_env['QTDIR']
|
|
||||||
qt_env.Tool('qt3')
|
|
||||||
|
|
||||||
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
|
|
||||||
qt_flags = [
|
|
||||||
"-D_REENTRANT",
|
|
||||||
"-DQT_NO_DEBUG",
|
|
||||||
"-DQT_WIDGETS_LIB",
|
|
||||||
"-DQT_GUI_LIB",
|
|
||||||
"-DQT_CORE_LIB",
|
|
||||||
"-DQT_MESSAGELOGCONTEXT",
|
|
||||||
]
|
|
||||||
qt_env['CXXFLAGS'] += qt_flags
|
|
||||||
qt_env['LIBPATH'] += ['#selfdrive/ui', ]
|
|
||||||
qt_env['LIBS'] = qt_libs
|
|
||||||
|
|
||||||
if GetOption("clazy"):
|
|
||||||
checks = [
|
|
||||||
"level0",
|
|
||||||
"level1",
|
|
||||||
"no-range-loop",
|
|
||||||
"no-non-pod-global-static",
|
|
||||||
]
|
|
||||||
qt_env['CXX'] = 'clazy'
|
|
||||||
qt_env['ENV']['CLAZY_IGNORE_DIRS'] = qt_dirs[0]
|
|
||||||
qt_env['ENV']['CLAZY_CHECKS'] = ','.join(checks)
|
|
||||||
|
|
||||||
Export('env', 'qt_env', 'arch', 'real_arch')
|
|
||||||
|
|
||||||
# Build common module
|
# Build common module
|
||||||
SConscript(['common/SConscript'])
|
SConscript(['common/SConscript'])
|
||||||
Import('_common', '_gpucommon')
|
Import('_common')
|
||||||
|
|
||||||
common = [_common, 'json11', 'zmq']
|
common = [_common, 'json11', 'zmq']
|
||||||
gpucommon = [_gpucommon]
|
Export('common')
|
||||||
|
|
||||||
Export('common', 'gpucommon')
|
|
||||||
|
|
||||||
# Build messaging (cereal + msgq + socketmaster + their dependencies)
|
# Build messaging (cereal + msgq + socketmaster + their dependencies)
|
||||||
# Enable swaglog include in submodules
|
# Enable swaglog include in submodules
|
||||||
@@ -375,6 +231,5 @@ if Dir('#tools/cabana/').exists() and GetOption('extras'):
|
|||||||
if arch != "larch64":
|
if arch != "larch64":
|
||||||
SConscript(['tools/cabana/SConscript'])
|
SConscript(['tools/cabana/SConscript'])
|
||||||
|
|
||||||
external_sconscript = GetOption('external_sconscript')
|
|
||||||
if external_sconscript:
|
env.CompilationDatabase('compile_commands.json')
|
||||||
SConscript([external_sconscript])
|
|
||||||
|
|||||||
+15
-1
@@ -369,6 +369,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
|||||||
leadOne @2 :LeadData;
|
leadOne @2 :LeadData;
|
||||||
leadTwo @3 :LeadData;
|
leadTwo @3 :LeadData;
|
||||||
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
||||||
|
speed @5 :Float32;
|
||||||
|
|
||||||
struct Param {
|
struct Param {
|
||||||
key @0 :Text;
|
key @0 :Text;
|
||||||
@@ -454,7 +455,20 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
struct Navigationd @0xcb9fd56c7057593a {
|
||||||
|
upcomingTurn @0 :Text;
|
||||||
|
currentSpeedLimit @1 :UInt16;
|
||||||
|
bannerInstructions @2 :Text;
|
||||||
|
distanceFromRoute @3 :Float32;
|
||||||
|
allManeuvers @4 :List(Maneuver);
|
||||||
|
valid @5 :Bool;
|
||||||
|
|
||||||
|
struct Maneuver {
|
||||||
|
distance @0 :Float32;
|
||||||
|
type @1 :Text;
|
||||||
|
modifier @2 :Text;
|
||||||
|
instruction @3 :Text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomReserved11 @0xc2243c65e0340384 {
|
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||||
|
|||||||
+9
-6
@@ -918,6 +918,8 @@ struct ControlsState @0x97ff69c53601abf1 {
|
|||||||
saturated @7 :Bool;
|
saturated @7 :Bool;
|
||||||
actualLateralAccel @9 :Float32;
|
actualLateralAccel @9 :Float32;
|
||||||
desiredLateralAccel @10 :Float32;
|
desiredLateralAccel @10 :Float32;
|
||||||
|
desiredLateralJerk @11 :Float32;
|
||||||
|
version @12 :Int32;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LateralLQRState {
|
struct LateralLQRState {
|
||||||
@@ -2146,13 +2148,10 @@ struct Joystick {
|
|||||||
struct DriverStateV2 {
|
struct DriverStateV2 {
|
||||||
frameId @0 :UInt32;
|
frameId @0 :UInt32;
|
||||||
modelExecutionTime @1 :Float32;
|
modelExecutionTime @1 :Float32;
|
||||||
dspExecutionTimeDEPRECATED @2 :Float32;
|
|
||||||
gpuExecutionTime @8 :Float32;
|
gpuExecutionTime @8 :Float32;
|
||||||
rawPredictions @3 :Data;
|
rawPredictions @3 :Data;
|
||||||
|
|
||||||
poorVisionProb @4 :Float32;
|
|
||||||
wheelOnRightProb @5 :Float32;
|
wheelOnRightProb @5 :Float32;
|
||||||
|
|
||||||
leftDriverData @6 :DriverData;
|
leftDriverData @6 :DriverData;
|
||||||
rightDriverData @7 :DriverData;
|
rightDriverData @7 :DriverData;
|
||||||
|
|
||||||
@@ -2167,10 +2166,13 @@ struct DriverStateV2 {
|
|||||||
leftBlinkProb @7 :Float32;
|
leftBlinkProb @7 :Float32;
|
||||||
rightBlinkProb @8 :Float32;
|
rightBlinkProb @8 :Float32;
|
||||||
sunglassesProb @9 :Float32;
|
sunglassesProb @9 :Float32;
|
||||||
occludedProb @10 :Float32;
|
|
||||||
readyProb @11 :List(Float32);
|
|
||||||
notReadyProb @12 :List(Float32);
|
notReadyProb @12 :List(Float32);
|
||||||
|
occludedProbDEPRECATED @10 :Float32;
|
||||||
|
readyProbDEPRECATED @11 :List(Float32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dspExecutionTimeDEPRECATED @2 :Float32;
|
||||||
|
poorVisionProbDEPRECATED @4 :Float32;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DriverStateDEPRECATED @0xb83c6cc593ed0a00 {
|
struct DriverStateDEPRECATED @0xb83c6cc593ed0a00 {
|
||||||
@@ -2222,6 +2224,7 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
|
|||||||
hiStdCount @14 :UInt32;
|
hiStdCount @14 :UInt32;
|
||||||
isActiveMode @16 :Bool;
|
isActiveMode @16 :Bool;
|
||||||
isRHD @4 :Bool;
|
isRHD @4 :Bool;
|
||||||
|
uncertainCount @19 :UInt32;
|
||||||
|
|
||||||
isPreviewDEPRECATED @15 :Bool;
|
isPreviewDEPRECATED @15 :Bool;
|
||||||
rhdCheckedDEPRECATED @5 :Bool;
|
rhdCheckedDEPRECATED @5 :Bool;
|
||||||
@@ -2632,7 +2635,7 @@ struct Event {
|
|||||||
carStateSP @114 :Custom.CarStateSP;
|
carStateSP @114 :Custom.CarStateSP;
|
||||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||||
customReserved10 @136 :Custom.CustomReserved10;
|
navigationd @136 :Custom.Navigationd;
|
||||||
customReserved11 @137 :Custom.CustomReserved11;
|
customReserved11 @137 :Custom.CustomReserved11;
|
||||||
customReserved12 @138 :Custom.CustomReserved12;
|
customReserved12 @138 :Custom.CustomReserved12;
|
||||||
customReserved13 @139 :Custom.CustomReserved13;
|
customReserved13 @139 :Custom.CustomReserved13;
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ _services: dict[str, tuple] = {
|
|||||||
"carStateSP": (True, 100., 10),
|
"carStateSP": (True, 100., 10),
|
||||||
"liveMapDataSP": (True, 1., 1),
|
"liveMapDataSP": (True, 1., 1),
|
||||||
"modelDataV2SP": (True, 20.),
|
"modelDataV2SP": (True, 20.),
|
||||||
|
"navigationd": (True, 3.),
|
||||||
"liveLocationKalman": (True, 20.),
|
"liveLocationKalman": (True, 20.),
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
|
|||||||
+3
-9
@@ -4,18 +4,12 @@ common_libs = [
|
|||||||
'params.cc',
|
'params.cc',
|
||||||
'swaglog.cc',
|
'swaglog.cc',
|
||||||
'util.cc',
|
'util.cc',
|
||||||
'watchdog.cc',
|
'ratekeeper.cc',
|
||||||
'ratekeeper.cc'
|
|
||||||
]
|
|
||||||
|
|
||||||
_common = env.Library('common', common_libs, LIBS="json11")
|
|
||||||
|
|
||||||
files = [
|
|
||||||
'clutil.cc',
|
'clutil.cc',
|
||||||
]
|
]
|
||||||
|
|
||||||
_gpucommon = env.Library('gpucommon', files)
|
_common = env.Library('common', common_libs, LIBS="json11")
|
||||||
Export('_common', '_gpucommon')
|
Export('_common')
|
||||||
|
|
||||||
if GetOption('extras'):
|
if GetOption('extras'):
|
||||||
env.Program('tests/test_common',
|
env.Program('tests/test_common',
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ class Api:
|
|||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
return self.service.post(*args, **kwargs)
|
return self.service.post(*args, **kwargs)
|
||||||
|
|
||||||
def get_token(self, expiry_hours=1):
|
def get_token(self, payload_extra=None, expiry_hours=1):
|
||||||
return self.service.get_token(expiry_hours)
|
return self.service.get_token(payload_extra, expiry_hours)
|
||||||
|
|
||||||
|
|
||||||
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
|
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
|
||||||
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
|
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
|
||||||
|
|
||||||
|
|
||||||
|
def get_key_pair():
|
||||||
|
return CommaConnectApi(None).get_key_pair()
|
||||||
|
|||||||
+20
-6
@@ -1,18 +1,22 @@
|
|||||||
import jwt
|
import jwt
|
||||||
|
import os
|
||||||
import requests
|
import requests
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, UTC
|
||||||
from openpilot.system.hardware.hw import Paths
|
from openpilot.system.hardware.hw import Paths
|
||||||
from openpilot.system.version import get_version
|
from openpilot.system.version import get_version
|
||||||
|
|
||||||
|
# name : jwt signature algorithm
|
||||||
|
KEYS = {"id_rsa" : "RS256",
|
||||||
|
"id_ecdsa" : "ES256"}
|
||||||
|
|
||||||
|
|
||||||
class BaseApi:
|
class BaseApi:
|
||||||
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
|
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
|
||||||
self.dongle_id = dongle_id
|
self.dongle_id = dongle_id
|
||||||
self.api_host = api_host
|
self.api_host = api_host
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
with open(f'{Paths.persist_root()}/comma/id_rsa') as f:
|
self.jwt_algorithm, self.private_key, _ = self.get_key_pair()
|
||||||
self.private_key = f.read()
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
return self.request('GET', *args, **kwargs)
|
return self.request('GET', *args, **kwargs)
|
||||||
@@ -23,7 +27,7 @@ class BaseApi:
|
|||||||
def request(self, method, endpoint, timeout=None, access_token=None, **params):
|
def request(self, method, endpoint, timeout=None, access_token=None, **params):
|
||||||
return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
|
return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
|
||||||
|
|
||||||
def _get_token(self, expiry_hours=1, **extra_payload):
|
def _get_token(self, payload_extra=None, expiry_hours=1, **extra_payload):
|
||||||
now = datetime.now(UTC).replace(tzinfo=None)
|
now = datetime.now(UTC).replace(tzinfo=None)
|
||||||
payload = {
|
payload = {
|
||||||
'identity': self.dongle_id,
|
'identity': self.dongle_id,
|
||||||
@@ -32,13 +36,15 @@ class BaseApi:
|
|||||||
'exp': now + timedelta(hours=expiry_hours),
|
'exp': now + timedelta(hours=expiry_hours),
|
||||||
**extra_payload
|
**extra_payload
|
||||||
}
|
}
|
||||||
token = jwt.encode(payload, self.private_key, algorithm='RS256')
|
if payload_extra is not None:
|
||||||
|
payload.update(payload_extra)
|
||||||
|
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
|
||||||
if isinstance(token, bytes):
|
if isinstance(token, bytes):
|
||||||
token = token.decode('utf8')
|
token = token.decode('utf8')
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def get_token(self, expiry_hours=1):
|
def get_token(self, payload_extra=None, expiry_hours=1):
|
||||||
return self._get_token(expiry_hours)
|
return self._get_token(payload_extra, expiry_hours)
|
||||||
|
|
||||||
def remove_non_ascii_chars(self, text):
|
def remove_non_ascii_chars(self, text):
|
||||||
normalized_text = unicodedata.normalize('NFD', text)
|
normalized_text = unicodedata.normalize('NFD', text)
|
||||||
@@ -54,3 +60,11 @@ class BaseApi:
|
|||||||
headers['User-Agent'] = self.user_agent + version
|
headers['User-Agent'] = self.user_agent + version
|
||||||
|
|
||||||
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
|
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_key_pair():
|
||||||
|
for key in KEYS:
|
||||||
|
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
|
||||||
|
with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public:
|
||||||
|
return KEYS[key], private.read(), public.read()
|
||||||
|
return None, None, None
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# remove all keys that end in DEPRECATED
|
|
||||||
def strip_deprecated_keys(d):
|
|
||||||
for k in list(d.keys()):
|
|
||||||
if isinstance(k, str):
|
|
||||||
if k.endswith('DEPRECATED'):
|
|
||||||
d.pop(k)
|
|
||||||
elif isinstance(d[k], dict):
|
|
||||||
strip_deprecated_keys(d[k])
|
|
||||||
return d
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
from functools import cache
|
from functools import cache
|
||||||
import subprocess
|
import subprocess
|
||||||
from openpilot.common.run import run_cmd, run_cmd_default
|
from openpilot.common.utils import run_cmd, run_cmd_default
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
#define DEFAULT_MODEL "TCPv3 + gWMv9 (Default)"
|
#define DEFAULT_MODEL "The Cool People (Default)"
|
||||||
|
|||||||
+13
-1
@@ -66,7 +66,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
{"LanguageSetting", {PERSISTENT | BACKUP, STRING, "main_en"}},
|
{"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}},
|
||||||
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
|
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
|
||||||
{"LastGPSPosition", {PERSISTENT, STRING}},
|
{"LastGPSPosition", {PERSISTENT, STRING}},
|
||||||
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
|
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
|
||||||
@@ -97,6 +97,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
|
{"Offroad_DriverMonitoringUncertain", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||||
{"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
{"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||||
{"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
@@ -108,6 +109,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"RecordFront", {PERSISTENT | BACKUP, BOOL}},
|
{"RecordFront", {PERSISTENT | BACKUP, BOOL}},
|
||||||
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
|
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
|
||||||
{"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}},
|
{"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}},
|
||||||
|
{"ShowDebugInfo", {PERSISTENT, BOOL}},
|
||||||
{"RouteCount", {PERSISTENT, INT, "0"}},
|
{"RouteCount", {PERSISTENT, INT, "0"}},
|
||||||
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
{"SshEnabled", {PERSISTENT | BACKUP, BOOL}},
|
{"SshEnabled", {PERSISTENT | BACKUP, BOOL}},
|
||||||
@@ -171,6 +173,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"sunnypilot_ui", {PERSISTENT, BOOL, "1"}},
|
||||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
// MADS params
|
// MADS params
|
||||||
@@ -187,6 +190,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||||
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
||||||
|
|
||||||
|
// Navigation params
|
||||||
|
{"AllowNavigation", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"MapboxFavorites", {PERSISTENT | BACKUP, STRING}},
|
||||||
|
{"MapboxToken", {PERSISTENT | BACKUP, STRING}},
|
||||||
|
{"MapboxSettings", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
|
{"MapboxRoute", {PERSISTENT, STRING}},
|
||||||
|
{"MapboxRecompute", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"NavDesiresAllowed", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
// Neural Network Lateral Control
|
// Neural Network Lateral Control
|
||||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
|
|||||||
+6
-7
@@ -2,11 +2,10 @@ import numpy as np
|
|||||||
from numbers import Number
|
from numbers import Number
|
||||||
|
|
||||||
class PIDController:
|
class PIDController:
|
||||||
def __init__(self, k_p, k_i, k_f=0., k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
|
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
|
||||||
self._k_p = k_p
|
self._k_p = k_p
|
||||||
self._k_i = k_i
|
self._k_i = k_i
|
||||||
self._k_d = k_d
|
self._k_d = k_d
|
||||||
self.k_f = k_f # feedforward gain
|
|
||||||
if isinstance(self._k_p, Number):
|
if isinstance(self._k_p, Number):
|
||||||
self._k_p = [[0], [self._k_p]]
|
self._k_p = [[0], [self._k_p]]
|
||||||
if isinstance(self._k_i, Number):
|
if isinstance(self._k_i, Number):
|
||||||
@@ -16,7 +15,7 @@ class PIDController:
|
|||||||
|
|
||||||
self.set_limits(pos_limit, neg_limit)
|
self.set_limits(pos_limit, neg_limit)
|
||||||
|
|
||||||
self.i_rate = 1.0 / rate
|
self.i_dt = 1.0 / rate
|
||||||
self.speed = 0.0
|
self.speed = 0.0
|
||||||
|
|
||||||
self.reset()
|
self.reset()
|
||||||
@@ -46,12 +45,12 @@ class PIDController:
|
|||||||
|
|
||||||
def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False):
|
def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False):
|
||||||
self.speed = speed
|
self.speed = speed
|
||||||
self.p = float(error) * self.k_p
|
self.p = self.k_p * float(error)
|
||||||
self.f = feedforward * self.k_f
|
self.d = self.k_d * error_rate
|
||||||
self.d = error_rate * self.k_d
|
self.f = feedforward
|
||||||
|
|
||||||
if not freeze_integrator:
|
if not freeze_integrator:
|
||||||
i = self.i + error * self.k_i * self.i_rate
|
i = self.i + self.k_i * self.i_dt * error
|
||||||
|
|
||||||
# Don't allow windup if already clipping
|
# Don't allow windup if already clipping
|
||||||
test_control = self.p + i + self.d + self.f
|
test_control = self.p + i + self.d + self.f
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import time
|
|
||||||
import functools
|
|
||||||
|
|
||||||
from openpilot.common.swaglog import cloudlog
|
|
||||||
|
|
||||||
|
|
||||||
def retry(attempts=3, delay=1.0, ignore_failure=False):
|
|
||||||
def decorator(func):
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
for _ in range(attempts):
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
except Exception:
|
|
||||||
cloudlog.exception(f"{func.__name__} failed, trying again")
|
|
||||||
time.sleep(delay)
|
|
||||||
|
|
||||||
if ignore_failure:
|
|
||||||
cloudlog.error(f"{func.__name__} failed after retry")
|
|
||||||
else:
|
|
||||||
raise Exception(f"{func.__name__} failed after retry")
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
@retry(attempts=10)
|
|
||||||
def abc():
|
|
||||||
raise ValueError("abc failed :(")
|
|
||||||
abc()
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from subprocess import Popen, PIPE, TimeoutExpired
|
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
|
|
||||||
return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str:
|
|
||||||
try:
|
|
||||||
return run_cmd(cmd, cwd=cwd, env=env)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def managed_proc(cmd: list[str], env: dict[str, str]):
|
|
||||||
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
|
|
||||||
try:
|
|
||||||
yield proc
|
|
||||||
finally:
|
|
||||||
if proc.poll() is None:
|
|
||||||
proc.terminate()
|
|
||||||
try:
|
|
||||||
proc.wait(timeout=5)
|
|
||||||
except TimeoutExpired:
|
|
||||||
proc.kill()
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from openpilot.common.file_helpers import atomic_write_in_dir
|
from openpilot.common.utils import atomic_write_in_dir
|
||||||
|
|
||||||
|
|
||||||
class TestFileHelpers:
|
class TestFileHelpers:
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import io
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import functools
|
||||||
|
from subprocess import Popen, PIPE, TimeoutExpired
|
||||||
import zstandard as zstd
|
import zstandard as zstd
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
|
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
|
||||||
|
|
||||||
|
|
||||||
class CallbackReader:
|
class CallbackReader:
|
||||||
@@ -27,7 +32,7 @@ class CallbackReader:
|
|||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str = None, newline: str = None,
|
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
|
||||||
overwrite: bool = False):
|
overwrite: bool = False):
|
||||||
"""Write to a file atomically using a temporary file in the same directory as the destination file."""
|
"""Write to a file atomically using a temporary file in the same directory as the destination file."""
|
||||||
dir_name = os.path.dirname(path)
|
dir_name = os.path.dirname(path)
|
||||||
@@ -56,3 +61,58 @@ def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.Buffered
|
|||||||
compressed_size = compressed_stream.tell()
|
compressed_size = compressed_stream.tell()
|
||||||
compressed_stream.seek(0)
|
compressed_stream.seek(0)
|
||||||
return compressed_stream, compressed_size
|
return compressed_stream, compressed_size
|
||||||
|
|
||||||
|
|
||||||
|
# remove all keys that end in DEPRECATED
|
||||||
|
def strip_deprecated_keys(d):
|
||||||
|
for k in list(d.keys()):
|
||||||
|
if isinstance(k, str):
|
||||||
|
if k.endswith('DEPRECATED'):
|
||||||
|
d.pop(k)
|
||||||
|
elif isinstance(d[k], dict):
|
||||||
|
strip_deprecated_keys(d[k])
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
|
||||||
|
return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str:
|
||||||
|
try:
|
||||||
|
return run_cmd(cmd, cwd=cwd, env=env)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def managed_proc(cmd: list[str], env: dict[str, str]):
|
||||||
|
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
|
||||||
|
try:
|
||||||
|
yield proc
|
||||||
|
finally:
|
||||||
|
if proc.poll() is None:
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
def retry(attempts=3, delay=1.0, ignore_failure=False):
|
||||||
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
for _ in range(attempts):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
cloudlog.exception(f"{func.__name__} failed, trying again")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if ignore_failure:
|
||||||
|
cloudlog.error(f"{func.__name__} failed after retry")
|
||||||
|
else:
|
||||||
|
raise Exception(f"{func.__name__} failed after retry")
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
#define COMMA_VERSION "0.10.1"
|
#define COMMA_VERSION "0.10.2"
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#include <string>
|
|
||||||
|
|
||||||
#include "common/watchdog.h"
|
|
||||||
#include "common/util.h"
|
|
||||||
#include "system/hardware/hw.h"
|
|
||||||
|
|
||||||
const std::string watchdog_fn_prefix = Path::shm_path() + "/wd_"; // + <pid>
|
|
||||||
|
|
||||||
bool watchdog_kick(uint64_t ts) {
|
|
||||||
static std::string fn = watchdog_fn_prefix + std::to_string(getpid());
|
|
||||||
return util::write_file(fn.c_str(), &ts, sizeof(ts), O_WRONLY | O_CREAT) > 0;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
bool watchdog_kick(uint64_t ts);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import struct
|
|
||||||
from openpilot.system.hardware.hw import Paths
|
|
||||||
|
|
||||||
WATCHDOG_FN = f"{Paths.shm_path()}/wd_"
|
|
||||||
_LAST_KICK = 0.0
|
|
||||||
|
|
||||||
def kick_watchdog():
|
|
||||||
global _LAST_KICK
|
|
||||||
current_time = time.monotonic()
|
|
||||||
|
|
||||||
if current_time - _LAST_KICK < 1.0:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f:
|
|
||||||
f.write(struct.pack('<Q', int(current_time * 1e9)))
|
|
||||||
f.flush()
|
|
||||||
_LAST_KICK = current_time
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# CarState signals
|
||||||
|
|
||||||
|
## Required for basic lateral control
|
||||||
|
|
||||||
|
* `brakePressed`
|
||||||
|
* `cruiseState`
|
||||||
|
* `doorOpen`
|
||||||
|
* `espDisabled`
|
||||||
|
* `gasPressed`
|
||||||
|
* `gearShifter`
|
||||||
|
* `leftBlinker` / `rightBlinker`
|
||||||
|
* `seatbeltUnlatched`
|
||||||
|
* `standstill`
|
||||||
|
* `steeringAngleDeg`
|
||||||
|
* `steeringPressed`
|
||||||
|
* `steeringTorque`
|
||||||
|
* `steerFaultPermanent`
|
||||||
|
* `steerFaultTemporary`
|
||||||
|
* `vCruise`
|
||||||
|
* `wheelSpeeds.[fl|fr|rl|rr]`: Speed of each of the car's four wheels, in m/s. The car's CAN bus often broadcasts the
|
||||||
|
speed in kph, so the helper function `parse_wheel_speeds` performs this conversion by default.
|
||||||
|
|
||||||
|
## Recommended / Required for openpilot longitudinal control
|
||||||
|
|
||||||
|
* `accFaulted`
|
||||||
|
* `espActive`
|
||||||
|
* `parkingBrake`
|
||||||
|
|
||||||
|
## Application Dependent
|
||||||
|
|
||||||
|
* `blockPcmEnable`
|
||||||
|
* `buttonEnable`
|
||||||
|
* `brakeHoldActive`
|
||||||
|
* `carFaultedNonCritical`
|
||||||
|
* `invalidLkasSetting`
|
||||||
|
* `lowSpeedAlert`
|
||||||
|
* `regenBraking`
|
||||||
|
* `steeringAngleOffsetDeg`
|
||||||
|
* `steeringDisengage`
|
||||||
|
* `steeringTorqueEps`
|
||||||
|
* `stockLkas`
|
||||||
|
* `vCruiseCluster`
|
||||||
|
* `vEgoCluster`
|
||||||
|
* `vehicleSensorsInvalid`
|
||||||
|
|
||||||
|
## Automatically populated
|
||||||
|
|
||||||
|
* `buttonEvents`
|
||||||
|
|
||||||
|
These values are populated automatically by `parse_wheel_speeds`:
|
||||||
|
|
||||||
|
* `aEgo`: Acceleration of the ego vehicle, Kalman filtered derivative of `vEgo`.
|
||||||
|
* `vEgo`: Speed of the ego vehicle, Kalman filtered from `vEgoRaw`.
|
||||||
|
* `vEgoRaw`: Speed of the ego vehicle, based on the average of all four wheel speeds, unfiltered.
|
||||||
|
|
||||||
|
## Optional
|
||||||
|
|
||||||
|
* `brake`
|
||||||
|
* `charging`
|
||||||
|
* `fuelGauge`
|
||||||
|
* `leftBlindspot` / `rightBlindspot`
|
||||||
|
* `steeringRateDeg`
|
||||||
|
* `stockAeb`
|
||||||
|
* `stockFcw`
|
||||||
|
* `yawRate`
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Stimulus-Response Tests
|
||||||
|
|
||||||
|
These are example test drives that can help identify the CAN bus messaging necessary for ADAS control. Each scripted
|
||||||
|
test should be done in a separate route (ignition cycle). These tests are a guide, not necessarily exhaustive.
|
||||||
|
|
||||||
|
While testing, constant power to the comma device is highly recommended, using [comma power](https://comma.ai/shop/comma-power) if
|
||||||
|
necessary to make sure all test activity is fully captured and for ease of uploading. If constant power isn't
|
||||||
|
available, keep the ignition on for at least one minute after your test to make sure power loss doesn't result
|
||||||
|
in loss of the last minute of testing data.
|
||||||
|
|
||||||
|
## Stationary ignition-only tests, part 1
|
||||||
|
|
||||||
|
1. Ignition on, but don't start engine, remain in Park
|
||||||
|
2. Open and close each door in a defined order: driver, passenger, rear left, rear right
|
||||||
|
3. Re-enter the vehicle, close the driver's door, and fasten the driver's seatbelt
|
||||||
|
4. Slowly press and release the accelerator pedal 3 times
|
||||||
|
5. Slowly press and release the brake pedal 3 times
|
||||||
|
6. Hold the brake and move the gearshift to reverse, then neutral, then drive, then sport/eco/etc if applicable
|
||||||
|
7. Return to Park, ignition off
|
||||||
|
|
||||||
|
Brake-pressed information may show up in several messages and signals, both as on/off states and as a percentage or
|
||||||
|
pressure. It may reflect a switch on the driver's brake pedal, or a pressure-threshold state, or signals to turn on
|
||||||
|
the rear brake lights. Start by identifying all the potential signals, and confirm while driving with ACC later.
|
||||||
|
|
||||||
|
Locate signals for all four door states if possible, but some cars only expose the driver's door state on the ADAS bus.
|
||||||
|
Driver/passenger door signals may or may not change positions for LHD vs RHD cars. For cars where only the driver's
|
||||||
|
door signal is available, the same signal may follow the driver.
|
||||||
|
|
||||||
|
## Stationary ignition-only tests, part 2
|
||||||
|
|
||||||
|
1. Ignition on, but don't start engine, remain in Park
|
||||||
|
2. Press each ACC button in a defined order: main switch on/off, set, resume, cancel, accel, decel, gap adjust
|
||||||
|
3. Set the left turn signal for about five seconds
|
||||||
|
4. Operate the left turn signal one time in its touch-to-pass mode
|
||||||
|
5. Set the right turn signal for about five seconds
|
||||||
|
6. Operate the right turn signal one time in its touch-to-pass mode
|
||||||
|
7. Set the hazard / emergency indicator switch for about five seconds
|
||||||
|
8. Ignition off
|
||||||
|
|
||||||
|
Your vehicle may have a momentary-press main ACC switch or a physical toggle that remains set. Actual ACC engagement
|
||||||
|
isn't necessary for purposes of detecting the ACC button presses.
|
||||||
|
|
||||||
|
## Steering angle and steering torque tests
|
||||||
|
|
||||||
|
Power steering should be available. On ICE cars, engine RPM may be present.
|
||||||
|
|
||||||
|
1. Ignition on, start engine if applicable, remain in Park
|
||||||
|
2. Rotate the steering wheel as follows, with a few seconds pause between each step
|
||||||
|
* Start as close to exact center as possible
|
||||||
|
* Turn to 45 degrees right and hold
|
||||||
|
* Turn to 90 degrees right and hold
|
||||||
|
* Turn to 180 degrees right and hold
|
||||||
|
* Turn to full lock right and hold, with firm pressure against lock
|
||||||
|
* Release the wheel and allow it to bounce back slightly from lock
|
||||||
|
* Turn to 180 degrees left and hold
|
||||||
|
* Return to center and release
|
||||||
|
3. Ignition off
|
||||||
|
|
||||||
|
Performing the full test to the right, followed by an abbreviated test to the left, helps give additional confirmation
|
||||||
|
of signal scale, and sign/direction for both the steering wheel angle and driver input torque signals.
|
||||||
|
|
||||||
|
## Low speed / parking lot driving tests
|
||||||
|
|
||||||
|
Before this test, drive to a place like an empty parking lot where you are free to drive in a series of curves.
|
||||||
|
|
||||||
|
1. Ignition on, start engine if applicable, prepare to drive
|
||||||
|
2. Slowly (10-20mph at most) drive a figure-8 if possible, or at least one sharp left and one sharp right.
|
||||||
|
3. Come to a complete stop
|
||||||
|
4. When and where safe, drive in reverse for a short distance (10-15 feet)
|
||||||
|
5. Park the car in a safe place, ignition off
|
||||||
|
|
||||||
|
## High speed / highway driving tests
|
||||||
|
|
||||||
|
Select a place and time where you can safely set cruise control at normal travel speeds with little interference from
|
||||||
|
traffic ahead, and safely test the response of your factory lane guidance system.
|
||||||
|
|
||||||
|
1. Ignition on, start engine if applicable, prepare to drive
|
||||||
|
2. When safely able, engage adaptive cruise control below 50 mph
|
||||||
|
3. When safely able, use the ACC buttons to accelerate to 50mph, then 55mph, then 60mph
|
||||||
|
4. Disengage adaptive cruise
|
||||||
|
5. When safely able, allow your factory lane guidance to prevent lane departures, 2-3 times on both the left and right
|
||||||
|
|
||||||
|
The series of setpoints can be adjusted to local traffic regulations, and of course metric units. The specific cruise
|
||||||
|
setpoints are useful for locating the ACC HUD signals later, and confirming their precise scaling. When the car reaches
|
||||||
|
and holds the setpoint, that can also provide additional confirmation of wheel speed scaling.
|
||||||
+10
-1
@@ -6,8 +6,17 @@ export NUMEXPR_NUM_THREADS=1
|
|||||||
export OPENBLAS_NUM_THREADS=1
|
export OPENBLAS_NUM_THREADS=1
|
||||||
export VECLIB_MAXIMUM_THREADS=1
|
export VECLIB_MAXIMUM_THREADS=1
|
||||||
|
|
||||||
|
# models get lower priority than ui
|
||||||
|
# - ui is ~5ms
|
||||||
|
# - modeld is 20ms
|
||||||
|
# - DM is 10ms
|
||||||
|
# in order to run ui at 60fps (16.67ms), we need to allow
|
||||||
|
# it to preempt the model workloads. we have enough
|
||||||
|
# headroom for this until ui is moved to the CPU.
|
||||||
|
export QCOM_PRIORITY=12
|
||||||
|
|
||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="13.1"
|
export AGNOS_VERSION="15"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: c7126f8ba6...b054629f5e
+1
-1
Submodule panda updated: 69ab12ee2a...dee9061b2a
+10
-5
@@ -23,7 +23,7 @@ dependencies = [
|
|||||||
# core
|
# core
|
||||||
"cffi",
|
"cffi",
|
||||||
"scons",
|
"scons",
|
||||||
"pycapnp",
|
"pycapnp==2.1.0",
|
||||||
"Cython",
|
"Cython",
|
||||||
"setuptools",
|
"setuptools",
|
||||||
"numpy >=2.0",
|
"numpy >=2.0",
|
||||||
@@ -72,7 +72,9 @@ dependencies = [
|
|||||||
"zstandard",
|
"zstandard",
|
||||||
|
|
||||||
# ui
|
# ui
|
||||||
|
"raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186
|
||||||
"qrcode",
|
"qrcode",
|
||||||
|
"mapbox-earcut",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -119,7 +121,6 @@ dev = [
|
|||||||
"tabulate",
|
"tabulate",
|
||||||
"types-requests",
|
"types-requests",
|
||||||
"types-tabulate",
|
"types-tabulate",
|
||||||
"raylib",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
tools = [
|
tools = [
|
||||||
@@ -177,7 +178,7 @@ quiet-level = 3
|
|||||||
# if you've got a short variable name that's getting flagged, add it here
|
# if you've got a short variable name that's getting flagged, add it here
|
||||||
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite"
|
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite"
|
||||||
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
||||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.ts, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*"
|
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.11"
|
python_version = "3.11"
|
||||||
@@ -235,7 +236,6 @@ lint.ignore = [
|
|||||||
"B027",
|
"B027",
|
||||||
"B024",
|
"B024",
|
||||||
"NPY002", # new numpy random syntax is worse
|
"NPY002", # new numpy random syntax is worse
|
||||||
"UP038", # (x, y) -> x|y for isinstance
|
|
||||||
]
|
]
|
||||||
line-length = 160
|
line-length = 160
|
||||||
target-version ="py311"
|
target-version ="py311"
|
||||||
@@ -263,8 +263,13 @@ lint.flake8-implicit-str-concat.allow-multiline = false
|
|||||||
"tools".msg = "Use openpilot.tools"
|
"tools".msg = "Use openpilot.tools"
|
||||||
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
|
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
|
||||||
"unittest".msg = "Use pytest"
|
"unittest".msg = "Use pytest"
|
||||||
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
|
|
||||||
"time.time".msg = "Use time.monotonic"
|
"time.time".msg = "Use time.monotonic"
|
||||||
|
|
||||||
|
# raylib banned APIs
|
||||||
|
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
|
||||||
|
"pyray.is_mouse_button_pressed".msg = "This can miss events. Use Widget._handle_mouse_press"
|
||||||
|
"pyray.is_mouse_button_released".msg = "This can miss events. Use Widget._handle_mouse_release"
|
||||||
|
"pyray.draw_text".msg = "Use a function (such as rl.draw_font_ex) that takes font as an argument"
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "preserve"
|
quote-style = "preserve"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ from openpilot.common.basedir import BASEDIR
|
|||||||
|
|
||||||
|
|
||||||
DIRS = ['cereal', 'openpilot']
|
DIRS = ['cereal', 'openpilot']
|
||||||
EXTS = ['.png', '.py', '.ttf', '.capnp']
|
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo']
|
||||||
INTERPRETER = '/usr/bin/env python3'
|
INTERPRETER = '/usr/bin/env python3'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
|
|||||||
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
|
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
|
||||||
SConscript(['locationd/SConscript'])
|
SConscript(['locationd/SConscript'])
|
||||||
SConscript(['modeld/SConscript'])
|
SConscript(['modeld/SConscript'])
|
||||||
SConscript(['ui/SConscript'])
|
SConscript(['ui/SConscript'])
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
*.cc
|
*.cc
|
||||||
|
fonts/*.fnt
|
||||||
|
fonts/*.png
|
||||||
translations_assets.qrc
|
translations_assets.qrc
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:93cdc4ee9aa40e2afceecc63da0ca05ec7aab4bec991ece51a6b52389f48a477
|
||||||
|
size 10788068
|
||||||
Executable
+128
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pyray as rl
|
||||||
|
|
||||||
|
FONT_DIR = Path(__file__).resolve().parent
|
||||||
|
SELFDRIVE_DIR = FONT_DIR.parents[1]
|
||||||
|
TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations"
|
||||||
|
LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
|
||||||
|
|
||||||
|
GLYPH_PADDING = 6
|
||||||
|
EXTRA_CHARS = "–‑✓×°§•€£¥"
|
||||||
|
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||||
|
|
||||||
|
|
||||||
|
def _languages():
|
||||||
|
if not LANGUAGES_FILE.exists():
|
||||||
|
return {}
|
||||||
|
with LANGUAGES_FILE.open(encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def _char_sets():
|
||||||
|
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
|
||||||
|
unifont = set(base)
|
||||||
|
|
||||||
|
for language, code in _languages().items():
|
||||||
|
unifont.update(language)
|
||||||
|
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
|
||||||
|
try:
|
||||||
|
chars = set(po_path.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
|
||||||
|
|
||||||
|
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
|
||||||
|
|
||||||
|
|
||||||
|
def _glyph_metrics(glyphs, rects, codepoints):
|
||||||
|
entries = []
|
||||||
|
min_offset_y, max_extent = None, 0
|
||||||
|
for idx, codepoint in enumerate(codepoints):
|
||||||
|
glyph = glyphs[idx]
|
||||||
|
rect = rects[idx]
|
||||||
|
width = int(round(rect.width))
|
||||||
|
height = int(round(rect.height))
|
||||||
|
offset_y = int(round(glyph.offsetY))
|
||||||
|
min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y)
|
||||||
|
max_extent = max(max_extent, offset_y + height)
|
||||||
|
entries.append({
|
||||||
|
"id": codepoint,
|
||||||
|
"x": int(round(rect.x)),
|
||||||
|
"y": int(round(rect.y)),
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"xoffset": int(round(glyph.offsetX)),
|
||||||
|
"yoffset": offset_y,
|
||||||
|
"xadvance": int(round(glyph.advanceX)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if min_offset_y is None:
|
||||||
|
raise RuntimeError("No glyphs were generated")
|
||||||
|
|
||||||
|
line_height = int(round(max_extent - min_offset_y))
|
||||||
|
base = int(round(max_extent))
|
||||||
|
return entries, line_height, base
|
||||||
|
|
||||||
|
|
||||||
|
def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries):
|
||||||
|
lines = [
|
||||||
|
f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0",
|
||||||
|
f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4",
|
||||||
|
f"page id=0 file=\"{atlas_name}\"",
|
||||||
|
f"chars count={len(entries)}",
|
||||||
|
]
|
||||||
|
for entry in entries:
|
||||||
|
lines.append(
|
||||||
|
("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " +
|
||||||
|
"xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry)
|
||||||
|
)
|
||||||
|
path.write_text("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
|
||||||
|
print(f"Processing {font_path.name}...")
|
||||||
|
|
||||||
|
font_size = {
|
||||||
|
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
|
||||||
|
}.get(font_path.name, 200)
|
||||||
|
|
||||||
|
data = font_path.read_bytes()
|
||||||
|
file_buf = rl.ffi.new("unsigned char[]", data)
|
||||||
|
cp_buffer = rl.ffi.new("int[]", codepoints)
|
||||||
|
cp_ptr = rl.ffi.cast("int *", cp_buffer)
|
||||||
|
glyphs = rl.load_font_data(rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints), rl.FontType.FONT_DEFAULT)
|
||||||
|
if glyphs == rl.ffi.NULL:
|
||||||
|
raise RuntimeError("raylib failed to load font data")
|
||||||
|
|
||||||
|
rects_ptr = rl.ffi.new("Rectangle **")
|
||||||
|
image = rl.gen_image_font_atlas(glyphs, rects_ptr, len(codepoints), font_size, GLYPH_PADDING, 0)
|
||||||
|
if image.width == 0 or image.height == 0:
|
||||||
|
raise RuntimeError("raylib returned an empty atlas")
|
||||||
|
|
||||||
|
rects = rects_ptr[0]
|
||||||
|
atlas_name = f"{font_path.stem}.png"
|
||||||
|
atlas_path = FONT_DIR / atlas_name
|
||||||
|
entries, line_height, base = _glyph_metrics(glyphs, rects, codepoints)
|
||||||
|
|
||||||
|
if not rl.export_image(image, atlas_path.as_posix()):
|
||||||
|
raise RuntimeError("Failed to export atlas image")
|
||||||
|
|
||||||
|
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base_cp, unifont_cp = _char_sets()
|
||||||
|
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
|
||||||
|
for font in fonts:
|
||||||
|
if "emoji" in font.name.lower():
|
||||||
|
continue
|
||||||
|
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
|
||||||
|
_process_font(font, glyphs)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9712a9bc089af7ddc06e0826aa84f2ee23ed2f1a1dddaf2a89c2483e753a8475
|
||||||
|
size 5321484
|
||||||
@@ -62,8 +62,8 @@ class TestCarInterfaces:
|
|||||||
# hypothesis also slows down significantly with just one more message draw
|
# hypothesis also slows down significantly with just one more message draw
|
||||||
LongControl(car_params, car_params_sp)
|
LongControl(car_params, car_params_sp)
|
||||||
if car_params.steerControlType == CarParams.SteerControlType.angle:
|
if car_params.steerControlType == CarParams.SteerControlType.angle:
|
||||||
LatControlAngle(car_params, car_params_sp, car_interface)
|
LatControlAngle(car_params, car_params_sp, car_interface, DT_CTRL)
|
||||||
elif car_params.lateralTuning.which() == 'pid':
|
elif car_params.lateralTuning.which() == 'pid':
|
||||||
LatControlPID(car_params, car_params_sp, car_interface)
|
LatControlPID(car_params, car_params_sp, car_interface, DT_CTRL)
|
||||||
elif car_params.lateralTuning.which() == 'torque':
|
elif car_params.lateralTuning.which() == 'torque':
|
||||||
LatControlTorque(car_params, car_params_sp, car_interface)
|
LatControlTorque(car_params, car_params_sp, car_interface, DT_CTRL)
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class TestCarModelBase(unittest.TestCase):
|
|||||||
if tuning == 'pid':
|
if tuning == 'pid':
|
||||||
self.assertTrue(len(self.CP.lateralTuning.pid.kpV))
|
self.assertTrue(len(self.CP.lateralTuning.pid.kpV))
|
||||||
elif tuning == 'torque':
|
elif tuning == 'torque':
|
||||||
self.assertTrue(self.CP.lateralTuning.torque.kf > 0)
|
self.assertTrue(self.CP.lateralTuning.torque.latAccelFactor > 0)
|
||||||
else:
|
else:
|
||||||
raise Exception("unknown tuning")
|
raise Exception("unknown tuning")
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from cereal import car, log
|
|||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority, Ratekeeper
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
from opendbc.car.car_helpers import interfaces
|
from opendbc.car.car_helpers import interfaces
|
||||||
@@ -19,6 +19,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
|||||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||||
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
|
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
|
||||||
|
from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS
|
||||||
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
|
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
|
||||||
|
|
||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
@@ -45,7 +46,7 @@ class Controls(ControlsExt, ModelStateBase):
|
|||||||
|
|
||||||
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
||||||
|
|
||||||
self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
|
self.sm = messaging.SubMaster(['liveDelay', 'liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
|
||||||
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
|
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
|
||||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
|
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
|
||||||
poll='selfdriveState')
|
poll='selfdriveState')
|
||||||
@@ -62,11 +63,11 @@ class Controls(ControlsExt, ModelStateBase):
|
|||||||
self.VM = VehicleModel(self.CP)
|
self.VM = VehicleModel(self.CP)
|
||||||
self.LaC: LatControl
|
self.LaC: LatControl
|
||||||
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
|
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||||
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI)
|
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI, DT_CTRL)
|
||||||
elif self.CP.lateralTuning.which() == 'pid':
|
elif self.CP.lateralTuning.which() == 'pid':
|
||||||
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI)
|
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI, DT_CTRL)
|
||||||
elif self.CP.lateralTuning.which() == 'torque':
|
elif self.CP.lateralTuning.which() == 'torque':
|
||||||
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI)
|
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI, DT_CTRL)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.sm.update(15)
|
self.sm.update(15)
|
||||||
@@ -139,11 +140,12 @@ class Controls(ControlsExt, ModelStateBase):
|
|||||||
# Reset desired curvature to current to avoid violating the limits on engage
|
# Reset desired curvature to current to avoid violating the limits on engage
|
||||||
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
|
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
|
||||||
self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)
|
self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)
|
||||||
|
lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
|
||||||
|
|
||||||
actuators.curvature = self.desired_curvature
|
actuators.curvature = self.desired_curvature
|
||||||
steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp,
|
steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp,
|
||||||
self.steer_limited_by_safety, self.desired_curvature,
|
self.steer_limited_by_safety, self.desired_curvature,
|
||||||
self.calibrated_pose, curvature_limited) # TODO what if not available
|
self.calibrated_pose, curvature_limited, lat_delay)
|
||||||
actuators.torque = float(steer)
|
actuators.torque = float(steer)
|
||||||
actuators.steeringAngleDeg = float(steeringAngleDeg)
|
actuators.steeringAngleDeg = float(steeringAngleDeg)
|
||||||
# Ensure no NaNs/Infs
|
# Ensure no NaNs/Infs
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from openpilot.common.constants import CV
|
|||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
|
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
|
||||||
|
from openpilot.sunnypilot.navd.navigation_desires.navigation_desires import NavigationDesires
|
||||||
|
|
||||||
LaneChangeState = log.LaneChangeState
|
LaneChangeState = log.LaneChangeState
|
||||||
LaneChangeDirection = log.LaneChangeDirection
|
LaneChangeDirection = log.LaneChangeDirection
|
||||||
@@ -51,6 +52,7 @@ class DesireHelper:
|
|||||||
self.alc = AutoLaneChangeController(self)
|
self.alc = AutoLaneChangeController(self)
|
||||||
self.lane_turn_controller = LaneTurnController(self)
|
self.lane_turn_controller = LaneTurnController(self)
|
||||||
self.lane_turn_direction = TurnDirection.none
|
self.lane_turn_direction = TurnDirection.none
|
||||||
|
self.navigation_desires = NavigationDesires()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_lane_change_direction(CS):
|
def get_lane_change_direction(CS):
|
||||||
@@ -143,3 +145,7 @@ class DesireHelper:
|
|||||||
self.desire = log.Desire.none
|
self.desire = log.Desire.none
|
||||||
|
|
||||||
self.alc.update_state()
|
self.alc.update_state()
|
||||||
|
|
||||||
|
nav_desire = self.navigation_desires.update(carstate, lateral_active)
|
||||||
|
if nav_desire != log.Desire.none and (self.desire == log.Desire.none or self.desire in (log.Desire.turnLeft, log.Desire.turnRight)):
|
||||||
|
self.desire = nav_desire
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def smooth_value(val, prev_val, tau, dt=DT_MDL):
|
|||||||
alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1
|
alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1
|
||||||
return alpha * val + (1 - alpha) * prev_val
|
return alpha * val + (1 - alpha) * prev_val
|
||||||
|
|
||||||
def clip_curvature(v_ego, prev_curvature, new_curvature, roll):
|
def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, bool]:
|
||||||
# This function respects ISO lateral jerk and acceleration limits + a max curvature
|
# This function respects ISO lateral jerk and acceleration limits + a max curvature
|
||||||
v_ego = max(v_ego, MIN_SPEED)
|
v_ego = max(v_ego, MIN_SPEED)
|
||||||
max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755
|
max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from abc import abstractmethod, ABC
|
from abc import abstractmethod, ABC
|
||||||
|
from openpilot.selfdrive.locationd.helpers import Pose
|
||||||
from openpilot.common.realtime import DT_CTRL
|
|
||||||
|
|
||||||
|
|
||||||
class LatControl(ABC):
|
class LatControl(ABC):
|
||||||
def __init__(self, CP, CP_SP, CI):
|
def __init__(self, CP, CP_SP, CI, dt):
|
||||||
self.sat_count_rate = 1.0 * DT_CTRL
|
self.dt = dt
|
||||||
self.sat_limit = CP.steerLimitTimer
|
self.sat_limit = CP.steerLimitTimer
|
||||||
self.sat_count = 0.
|
self.sat_time = 0.
|
||||||
self.sat_check_min_speed = 10.
|
self.sat_check_min_speed = 10.
|
||||||
|
|
||||||
# we define the steer torque scale as [-1.0...1.0]
|
# we define the steer torque scale as [-1.0...1.0]
|
||||||
self.steer_max = 1.0
|
self.steer_max = 1.0
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose,
|
||||||
|
curvature_limited: bool, lat_delay: float):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.sat_count = 0.
|
self.sat_time = 0.
|
||||||
|
|
||||||
def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited):
|
def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited):
|
||||||
# Saturated only if control output is not being limited by car torque/angle rate limits
|
# Saturated only if control output is not being limited by car torque/angle rate limits
|
||||||
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed:
|
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed:
|
||||||
self.sat_count += self.sat_count_rate
|
self.sat_time += self.dt
|
||||||
else:
|
else:
|
||||||
self.sat_count -= self.sat_count_rate
|
self.sat_time -= self.dt
|
||||||
self.sat_count = np.clip(self.sat_count, 0.0, self.sat_limit)
|
self.sat_time = np.clip(self.sat_time, 0.0, self.sat_limit)
|
||||||
return self.sat_count > (self.sat_limit - 1e-3)
|
return self.sat_time > (self.sat_limit - 1e-3)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
|
|||||||
|
|
||||||
|
|
||||||
class LatControlAngle(LatControl):
|
class LatControlAngle(LatControl):
|
||||||
def __init__(self, CP, CP_SP, CI):
|
def __init__(self, CP, CP_SP, CI, dt):
|
||||||
super().__init__(CP, CP_SP, CI)
|
super().__init__(CP, CP_SP, CI, dt)
|
||||||
self.sat_check_min_speed = 5.
|
self.sat_check_min_speed = 5.
|
||||||
self.use_steer_limited_by_safety = CP.brand == "tesla"
|
self.use_steer_limited_by_safety = CP.brand == "tesla"
|
||||||
|
|
||||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
|
||||||
angle_log = log.ControlsState.LateralAngleState.new_message()
|
angle_log = log.ControlsState.LateralAngleState.new_message()
|
||||||
|
|
||||||
if not active:
|
if not active:
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ from openpilot.common.pid import PIDController
|
|||||||
|
|
||||||
|
|
||||||
class LatControlPID(LatControl):
|
class LatControlPID(LatControl):
|
||||||
def __init__(self, CP, CP_SP, CI):
|
def __init__(self, CP, CP_SP, CI, dt):
|
||||||
super().__init__(CP, CP_SP, CI)
|
super().__init__(CP, CP_SP, CI, dt)
|
||||||
self.pid = PIDController((CP.lateralTuning.pid.kpBP, CP.lateralTuning.pid.kpV),
|
self.pid = PIDController((CP.lateralTuning.pid.kpBP, CP.lateralTuning.pid.kpV),
|
||||||
(CP.lateralTuning.pid.kiBP, CP.lateralTuning.pid.kiV),
|
(CP.lateralTuning.pid.kiBP, CP.lateralTuning.pid.kiV),
|
||||||
k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
|
pos_limit=self.steer_max, neg_limit=-self.steer_max)
|
||||||
|
self.ff_factor = CP.lateralTuning.pid.kf
|
||||||
self.get_steer_feedforward = CI.get_steer_feedforward_function()
|
self.get_steer_feedforward = CI.get_steer_feedforward_function()
|
||||||
|
|
||||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
|
||||||
pid_log = log.ControlsState.LateralPIDState.new_message()
|
pid_log = log.ControlsState.LateralPIDState.new_message()
|
||||||
pid_log.steeringAngleDeg = float(CS.steeringAngleDeg)
|
pid_log.steeringAngleDeg = float(CS.steeringAngleDeg)
|
||||||
pid_log.steeringRateDeg = float(CS.steeringRateDeg)
|
pid_log.steeringRateDeg = float(CS.steeringRateDeg)
|
||||||
@@ -30,7 +31,7 @@ class LatControlPID(LatControl):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# offset does not contribute to resistive torque
|
# offset does not contribute to resistive torque
|
||||||
ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
|
ff = self.ff_factor * self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
|
||||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||||
|
|
||||||
output_torque = self.pid.update(error,
|
output_torque = self.pid.update(error,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import math
|
import math
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
||||||
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
|
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
|
||||||
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||||
from openpilot.common.pid import PIDController
|
from openpilot.common.pid import PIDController
|
||||||
|
|
||||||
@@ -15,25 +17,34 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La
|
|||||||
# wheel slip, or to speed.
|
# wheel slip, or to speed.
|
||||||
|
|
||||||
# This controller applies torque to achieve desired lateral
|
# This controller applies torque to achieve desired lateral
|
||||||
# accelerations. To compensate for the low speed effects we
|
# accelerations. To compensate for the low speed effects the
|
||||||
# use a LOW_SPEED_FACTOR in the error. Additionally, there is
|
# proportional gain is increased at low speeds by the PID controller.
|
||||||
# friction in the steering wheel that needs to be overcome to
|
# Additionally, there is friction in the steering wheel that needs
|
||||||
# move it at all, this is compensated for too.
|
# to be overcome to move it at all, this is compensated for too.
|
||||||
|
|
||||||
LOW_SPEED_X = [0, 10, 20, 30]
|
KP = 1.0
|
||||||
LOW_SPEED_Y = [15, 13, 10, 5]
|
KI = 0.3
|
||||||
|
KD = 0.0
|
||||||
|
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||||
|
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||||
|
|
||||||
|
LP_FILTER_CUTOFF_HZ = 1.2
|
||||||
|
LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0
|
||||||
|
VERSION = 0
|
||||||
|
|
||||||
class LatControlTorque(LatControl):
|
class LatControlTorque(LatControl):
|
||||||
def __init__(self, CP, CP_SP, CI):
|
def __init__(self, CP, CP_SP, CI, dt):
|
||||||
super().__init__(CP, CP_SP, CI)
|
super().__init__(CP, CP_SP, CI, dt)
|
||||||
self.torque_params = CP.lateralTuning.torque.as_builder()
|
self.torque_params = CP.lateralTuning.torque.as_builder()
|
||||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||||
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
|
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
|
||||||
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
|
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt)
|
||||||
k_f=self.torque_params.kf)
|
|
||||||
self.update_limits()
|
self.update_limits()
|
||||||
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
|
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
|
||||||
|
self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt)
|
||||||
|
self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len)
|
||||||
|
self.previous_measurement = 0.0
|
||||||
|
self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
|
||||||
|
|
||||||
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
|
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
|
||||||
|
|
||||||
@@ -47,57 +58,68 @@ class LatControlTorque(LatControl):
|
|||||||
self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params),
|
self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params),
|
||||||
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
|
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
|
||||||
|
|
||||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
|
||||||
# Override torque params from extension
|
# Override torque params from extension
|
||||||
if self.extension.update_override_torque_params(self.torque_params):
|
if self.extension.update_override_torque_params(self.torque_params):
|
||||||
self.update_limits()
|
self.update_limits()
|
||||||
|
|
||||||
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
||||||
|
pid_log.version = VERSION
|
||||||
if not active:
|
if not active:
|
||||||
output_torque = 0.0
|
output_torque = 0.0
|
||||||
pid_log.active = False
|
pid_log.active = False
|
||||||
else:
|
else:
|
||||||
actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
||||||
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
|
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
|
||||||
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
|
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
|
||||||
|
|
||||||
desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
|
||||||
actual_lateral_accel = actual_curvature * CS.vEgo ** 2
|
|
||||||
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
|
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
|
||||||
|
|
||||||
low_speed_factor = np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2
|
delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len))
|
||||||
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
|
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
|
||||||
measurement = actual_lateral_accel + low_speed_factor * actual_curvature
|
# TODO factor out lateral jerk from error to later replace it with delay independent alternative
|
||||||
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation
|
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
||||||
|
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
|
||||||
|
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
|
||||||
|
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay
|
||||||
|
|
||||||
|
measurement = measured_curvature * CS.vEgo ** 2
|
||||||
|
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
|
||||||
|
self.previous_measurement = measurement
|
||||||
|
|
||||||
|
setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel
|
||||||
|
error = setpoint - measurement
|
||||||
|
|
||||||
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
||||||
pid_log.error = float(setpoint - measurement)
|
pid_log.error = float(error)
|
||||||
ff = gravity_adjusted_lateral_accel
|
ff = gravity_adjusted_future_lateral_accel
|
||||||
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
||||||
ff -= self.torque_params.latAccelOffset
|
ff -= self.torque_params.latAccelOffset
|
||||||
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
|
||||||
|
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||||
|
|
||||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||||
output_lataccel = self.pid.update(pid_log.error,
|
output_lataccel = self.pid.update(pid_log.error,
|
||||||
feedforward=ff,
|
-measurement_rate,
|
||||||
speed=CS.vEgo,
|
feedforward=ff,
|
||||||
freeze_integrator=freeze_integrator)
|
speed=CS.vEgo,
|
||||||
|
freeze_integrator=freeze_integrator)
|
||||||
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
|
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
|
||||||
|
|
||||||
# Lateral acceleration torque controller extension updates
|
# Lateral acceleration torque controller extension updates
|
||||||
# Overrides pid_log.error and output_torque
|
# Overrides pid_log.error and output_torque
|
||||||
pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
||||||
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
|
future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel,
|
||||||
desired_curvature, actual_curvature, steer_limited_by_safety, output_torque)
|
desired_curvature, measured_curvature, steer_limited_by_safety, output_torque)
|
||||||
|
|
||||||
pid_log.active = True
|
pid_log.active = True
|
||||||
pid_log.p = float(self.pid.p)
|
pid_log.p = float(self.pid.p)
|
||||||
pid_log.i = float(self.pid.i)
|
pid_log.i = float(self.pid.i)
|
||||||
pid_log.d = float(self.pid.d)
|
pid_log.d = float(self.pid.d)
|
||||||
pid_log.f = float(self.pid.f)
|
pid_log.f = float(self.pid.f)
|
||||||
pid_log.output = float(-output_torque) # TODO: log lat accel?
|
pid_log.output = float(-output_torque) # TODO: log lat accel?
|
||||||
pid_log.actualLateralAccel = float(actual_lateral_accel)
|
pid_log.actualLateralAccel = float(measurement)
|
||||||
pid_log.desiredLateralAccel = float(desired_lateral_accel)
|
pid_log.desiredLateralAccel = float(setpoint)
|
||||||
|
pid_log.desiredLateralJerk = float(desired_lateral_jerk)
|
||||||
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
|
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
|
||||||
|
|
||||||
# TODO left is positive in this convention
|
# TODO left is positive in this convention
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class LongControl:
|
|||||||
self.long_control_state = LongCtrlState.off
|
self.long_control_state = LongCtrlState.off
|
||||||
self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV),
|
self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV),
|
||||||
(CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV),
|
(CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV),
|
||||||
k_f=CP.longitudinalTuning.kf, rate=1 / DT_CTRL)
|
rate=1 / DT_CTRL)
|
||||||
self.last_output_accel = 0.0
|
self.last_output_accel = 0.0
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from opendbc.car.toyota.values import CAR as TOYOTA
|
|||||||
from opendbc.car.nissan.values import CAR as NISSAN
|
from opendbc.car.nissan.values import CAR as NISSAN
|
||||||
from opendbc.car.gm.values import CAR as GM
|
from opendbc.car.gm.values import CAR as GM
|
||||||
from opendbc.car.vehicle_model import VehicleModel
|
from opendbc.car.vehicle_model import VehicleModel
|
||||||
|
from openpilot.common.realtime import DT_CTRL
|
||||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||||
@@ -29,7 +30,7 @@ class TestLatControl:
|
|||||||
CP_SP = convert_to_capnp(CP_SP)
|
CP_SP = convert_to_capnp(CP_SP)
|
||||||
VM = VehicleModel(CP)
|
VM = VehicleModel(CP)
|
||||||
|
|
||||||
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI)
|
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
|
||||||
|
|
||||||
CS = car.CarState.new_message()
|
CS = car.CarState.new_message()
|
||||||
CS.vEgo = 30
|
CS.vEgo = 30
|
||||||
@@ -42,13 +43,13 @@ class TestLatControl:
|
|||||||
|
|
||||||
# Saturate for curvature limited and controller limited
|
# Saturate for curvature limited and controller limited
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True)
|
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2)
|
||||||
assert lac_log.saturated
|
assert lac_log.saturated
|
||||||
|
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False)
|
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2)
|
||||||
assert not lac_log.saturated
|
assert not lac_log.saturated
|
||||||
|
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False)
|
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2)
|
||||||
assert lac_log.saturated
|
assert lac_log.saturated
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from collections import defaultdict
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from cereal.services import SERVICE_LIST
|
from cereal.services import SERVICE_LIST
|
||||||
from openpilot.common.file_helpers import LOG_COMPRESSION_LEVEL
|
from openpilot.common.utils import LOG_COMPRESSION_LEVEL
|
||||||
from openpilot.tools.lib.logreader import LogReader
|
from openpilot.tools.lib.logreader import LogReader
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from collections import deque, defaultdict
|
from collections import deque, defaultdict
|
||||||
|
|
||||||
@@ -250,6 +251,8 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
|
|||||||
def main(demo=False):
|
def main(demo=False):
|
||||||
config_realtime_process([0, 1, 2, 3], 5)
|
config_realtime_process([0, 1, 2, 3], 5)
|
||||||
|
|
||||||
|
DEBUG = bool(int(os.getenv("DEBUG", "0")))
|
||||||
|
|
||||||
pm = messaging.PubMaster(['liveTorqueParameters'])
|
pm = messaging.PubMaster(['liveTorqueParameters'])
|
||||||
sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'liveCalibration', 'livePose', 'liveDelay'], poll='livePose')
|
sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'liveCalibration', 'livePose', 'liveDelay'], poll='livePose')
|
||||||
|
|
||||||
@@ -268,7 +271,7 @@ def main(demo=False):
|
|||||||
|
|
||||||
# 4Hz driven by livePose
|
# 4Hz driven by livePose
|
||||||
if sm.frame % 5 == 0:
|
if sm.frame % 5 == 0:
|
||||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks()))
|
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks(), with_points=DEBUG))
|
||||||
|
|
||||||
# Cache points every 60 seconds while onroad
|
# Cache points every 60 seconds while onroad
|
||||||
if sm.frame % 240 == 0:
|
if sm.frame % 240 == 0:
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations')
|
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
lenvCython = envCython.Clone()
|
lenvCython = envCython.Clone()
|
||||||
|
|
||||||
libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread']
|
libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread']
|
||||||
frameworks = []
|
frameworks = []
|
||||||
|
|
||||||
common_src = [
|
common_src = [
|
||||||
@@ -51,8 +51,8 @@ def tg_compile(flags, model_name):
|
|||||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||||
flags = {
|
flags = {
|
||||||
'larch64': 'DEV=QCOM',
|
'larch64': 'DEV=QCOM',
|
||||||
'Darwin': 'DEV=CPU IMAGE=0',
|
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
|
||||||
}.get(arch, 'DEV=LLVM IMAGE=0')
|
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||||
tg_compile(flags, model_name)
|
tg_compile(flags, model_name)
|
||||||
|
|
||||||
# Compile BIG model if USB GPU is available
|
# Compile BIG model if USB GPU is available
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from openpilot.system.hardware import TICI
|
from openpilot.system.hardware import TICI
|
||||||
os.environ['DEV'] = 'QCOM' if TICI else 'LLVM'
|
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||||
from tinygrad.tensor import Tensor
|
from tinygrad.tensor import Tensor
|
||||||
from tinygrad.dtype import dtypes
|
from tinygrad.dtype import dtypes
|
||||||
import math
|
import math
|
||||||
@@ -25,13 +25,13 @@ from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from
|
|||||||
MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE
|
MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE
|
||||||
CALIB_LEN = 3
|
CALIB_LEN = 3
|
||||||
FEATURE_LEN = 512
|
FEATURE_LEN = 512
|
||||||
OUTPUT_SIZE = 84 + FEATURE_LEN
|
OUTPUT_SIZE = 83 + FEATURE_LEN
|
||||||
|
|
||||||
PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld"
|
PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
|
MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
|
||||||
|
|
||||||
|
# TODO: slice from meta
|
||||||
class DriverStateResult(ctypes.Structure):
|
class DriverStateResult(ctypes.Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("face_orientation", ctypes.c_float*3),
|
("face_orientation", ctypes.c_float*3),
|
||||||
@@ -46,8 +46,8 @@ class DriverStateResult(ctypes.Structure):
|
|||||||
("left_blink_prob", ctypes.c_float),
|
("left_blink_prob", ctypes.c_float),
|
||||||
("right_blink_prob", ctypes.c_float),
|
("right_blink_prob", ctypes.c_float),
|
||||||
("sunglasses_prob", ctypes.c_float),
|
("sunglasses_prob", ctypes.c_float),
|
||||||
("occluded_prob", ctypes.c_float),
|
("_unused_c", ctypes.c_float),
|
||||||
("ready_prob", ctypes.c_float*4),
|
("_unused_d", ctypes.c_float*4),
|
||||||
("not_ready_prob", ctypes.c_float*2)]
|
("not_ready_prob", ctypes.c_float*2)]
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +55,6 @@ class DMonitoringModelResult(ctypes.Structure):
|
|||||||
_fields_ = [
|
_fields_ = [
|
||||||
("driver_state_lhd", DriverStateResult),
|
("driver_state_lhd", DriverStateResult),
|
||||||
("driver_state_rhd", DriverStateResult),
|
("driver_state_rhd", DriverStateResult),
|
||||||
("poor_vision_prob", ctypes.c_float),
|
|
||||||
("wheel_on_right_prob", ctypes.c_float),
|
("wheel_on_right_prob", ctypes.c_float),
|
||||||
("features", ctypes.c_float*FEATURE_LEN)]
|
("features", ctypes.c_float*FEATURE_LEN)]
|
||||||
|
|
||||||
@@ -107,8 +106,6 @@ def fill_driver_state(msg, ds_result: DriverStateResult):
|
|||||||
msg.leftBlinkProb = float(sigmoid(ds_result.left_blink_prob))
|
msg.leftBlinkProb = float(sigmoid(ds_result.left_blink_prob))
|
||||||
msg.rightBlinkProb = float(sigmoid(ds_result.right_blink_prob))
|
msg.rightBlinkProb = float(sigmoid(ds_result.right_blink_prob))
|
||||||
msg.sunglassesProb = float(sigmoid(ds_result.sunglasses_prob))
|
msg.sunglassesProb = float(sigmoid(ds_result.sunglasses_prob))
|
||||||
msg.occludedProb = float(sigmoid(ds_result.occluded_prob))
|
|
||||||
msg.readyProb = [float(sigmoid(x)) for x in ds_result.ready_prob]
|
|
||||||
msg.notReadyProb = [float(sigmoid(x)) for x in ds_result.not_ready_prob]
|
msg.notReadyProb = [float(sigmoid(x)) for x in ds_result.not_ready_prob]
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +116,6 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts:
|
|||||||
ds.frameId = frame_id
|
ds.frameId = frame_id
|
||||||
ds.modelExecutionTime = execution_time
|
ds.modelExecutionTime = execution_time
|
||||||
ds.gpuExecutionTime = gpu_execution_time
|
ds.gpuExecutionTime = gpu_execution_time
|
||||||
ds.poorVisionProb = float(sigmoid(model_result.poor_vision_prob))
|
|
||||||
ds.wheelOnRightProb = float(sigmoid(model_result.wheel_on_right_prob))
|
ds.wheelOnRightProb = float(sigmoid(model_result.wheel_on_right_prob))
|
||||||
ds.rawPredictions = model_output.tobytes() if SEND_RAW_PRED else b''
|
ds.rawPredictions = model_output.tobytes() if SEND_RAW_PRED else b''
|
||||||
fill_driver_state(ds.leftDriverData, model_result.driver_state_lhd)
|
fill_driver_state(ds.leftDriverData, model_result.driver_state_lhd)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from openpilot.system.hardware import TICI
|
from openpilot.system.hardware import TICI
|
||||||
os.environ['DEV'] = 'QCOM' if TICI else 'LLVM'
|
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||||
USBGPU = "USBGPU" in os.environ
|
USBGPU = "USBGPU" in os.environ
|
||||||
if USBGPU:
|
if USBGPU:
|
||||||
os.environ['DEV'] = 'AMD'
|
os.environ['DEV'] = 'AMD'
|
||||||
|
|||||||
@@ -62,6 +62,5 @@ Refer to **slice_outputs** and **parse_vision_outputs/parse_policy_outputs** in
|
|||||||
* (deprecated) distracted probabilities: 2
|
* (deprecated) distracted probabilities: 2
|
||||||
* using phone probability: 1
|
* using phone probability: 1
|
||||||
* distracted probability: 1
|
* distracted probability: 1
|
||||||
* common outputs 2
|
* common outputs 1
|
||||||
* poor camera vision probability: 1
|
|
||||||
* left hand drive probability: 1
|
* left hand drive probability: 1
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
fa69be01-b430-4504-9d72-7dcb058eb6dd
|
|
||||||
d9fb22d1c4fa3ca3d201dbc8edf1d0f0918e53e6
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:50efe6451a3fb3fa04b6bb0e846544533329bd46ecefe9e657e91214dee2aaeb
|
oid sha256:3a53626ab84757813fb16a1441704f2ae7192bef88c331bdc2415be6981d204f
|
||||||
size 7196502
|
size 7191776
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:76e7beaa7822219b019a4352ab111d0898abf72bd4b0d9520bd8dc68174e8c31
|
oid sha256:c5a1f0655ddf266ed42ad1980389d96f47cc5e756da1fa3ca1477a920bb9b157
|
||||||
size 13926460
|
size 13926324
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import numpy as np
|
|||||||
from cereal import car, log
|
from cereal import car, log
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from openpilot.selfdrive.selfdrived.events import Events
|
from openpilot.selfdrive.selfdrived.events import Events
|
||||||
|
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||||
from openpilot.common.realtime import DT_DMON
|
from openpilot.common.realtime import DT_DMON
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.stat_live import RunningStatFilter
|
from openpilot.common.stat_live import RunningStatFilter
|
||||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
|
|
||||||
EventName = log.OnroadEvent.EventName
|
EventName = log.OnroadEvent.EventName
|
||||||
|
|
||||||
@@ -34,12 +36,13 @@ class DRIVER_MONITOR_SETTINGS:
|
|||||||
self._SG_THRESHOLD = 0.9
|
self._SG_THRESHOLD = 0.9
|
||||||
self._BLINK_THRESHOLD = 0.865
|
self._BLINK_THRESHOLD = 0.865
|
||||||
|
|
||||||
self._EE_THRESH11 = 0.4
|
if HARDWARE.get_device_type() == 'mici':
|
||||||
|
self._EE_THRESH11 = 0.75
|
||||||
|
else:
|
||||||
|
self._EE_THRESH11 = 0.4
|
||||||
self._EE_THRESH12 = 15.0
|
self._EE_THRESH12 = 15.0
|
||||||
self._EE_MAX_OFFSET1 = 0.06
|
self._EE_MAX_OFFSET1 = 0.06
|
||||||
self._EE_MIN_OFFSET1 = 0.025
|
self._EE_MIN_OFFSET1 = 0.025
|
||||||
self._EE_THRESH21 = 0.01
|
|
||||||
self._EE_THRESH22 = 0.35
|
|
||||||
|
|
||||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||||
@@ -55,6 +58,9 @@ class DRIVER_MONITOR_SETTINGS:
|
|||||||
self._YAW_MAX_OFFSET = 0.289
|
self._YAW_MAX_OFFSET = 0.289
|
||||||
self._YAW_MIN_OFFSET = -0.0246
|
self._YAW_MIN_OFFSET = -0.0246
|
||||||
|
|
||||||
|
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
|
||||||
|
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / self._DT_DMON)
|
||||||
|
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / self._DT_DMON)
|
||||||
self._POSESTD_THRESHOLD = 0.3
|
self._POSESTD_THRESHOLD = 0.3
|
||||||
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
|
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
|
||||||
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
|
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
|
||||||
@@ -137,11 +143,8 @@ class DriverMonitoring:
|
|||||||
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
|
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
|
||||||
self.blink = DriverBlink()
|
self.blink = DriverBlink()
|
||||||
self.eev1 = 0.
|
self.eev1 = 0.
|
||||||
self.eev2 = 1.
|
|
||||||
self.ee1_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
|
self.ee1_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
|
||||||
self.ee2_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
|
|
||||||
self.ee1_calibrated = False
|
self.ee1_calibrated = False
|
||||||
self.ee2_calibrated = False
|
|
||||||
|
|
||||||
self.always_on = always_on
|
self.always_on = always_on
|
||||||
self.distracted_types = []
|
self.distracted_types = []
|
||||||
@@ -159,6 +162,9 @@ class DriverMonitoring:
|
|||||||
self.hi_stds = 0
|
self.hi_stds = 0
|
||||||
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||||
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||||
|
self.dcam_uncertain_cnt = 0
|
||||||
|
self.dcam_uncertain_alerted = False # once per drive
|
||||||
|
self.dcam_reset_cnt = 0
|
||||||
|
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
self.too_distracted = self.params.get_bool("DriverTooDistracted")
|
self.too_distracted = self.params.get_bool("DriverTooDistracted")
|
||||||
@@ -246,7 +252,7 @@ class DriverMonitoring:
|
|||||||
|
|
||||||
return distracted_types
|
return distracted_types
|
||||||
|
|
||||||
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged):
|
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill):
|
||||||
rhd_pred = driver_state.wheelOnRightProb
|
rhd_pred = driver_state.wheelOnRightProb
|
||||||
# calibrates only when there's movement and either face detected
|
# calibrates only when there's movement and either face detected
|
||||||
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
|
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
|
||||||
@@ -262,7 +268,7 @@ class DriverMonitoring:
|
|||||||
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
|
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
|
||||||
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
|
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
|
||||||
driver_data.faceOrientationStd, driver_data.facePositionStd,
|
driver_data.faceOrientationStd, driver_data.facePositionStd,
|
||||||
driver_data.readyProb, driver_data.notReadyProb)):
|
driver_data.notReadyProb)):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
|
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
|
||||||
@@ -279,7 +285,6 @@ class DriverMonitoring:
|
|||||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||||
self.eev1 = driver_data.notReadyProb[0]
|
self.eev1 = driver_data.notReadyProb[0]
|
||||||
self.eev2 = driver_data.readyProb[0]
|
|
||||||
|
|
||||||
self.distracted_types = self._get_distracted_types()
|
self.distracted_types = self._get_distracted_types()
|
||||||
self.driver_distracted = (DistractedType.DISTRACTED_E2E in self.distracted_types or DistractedType.DISTRACTED_POSE in self.distracted_types
|
self.driver_distracted = (DistractedType.DISTRACTED_E2E in self.distracted_types or DistractedType.DISTRACTED_POSE in self.distracted_types
|
||||||
@@ -293,12 +298,20 @@ class DriverMonitoring:
|
|||||||
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
||||||
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
||||||
self.ee1_offseter.push_and_update(self.eev1)
|
self.ee1_offseter.push_and_update(self.eev1)
|
||||||
self.ee2_offseter.push_and_update(self.eev2)
|
|
||||||
|
|
||||||
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
|
self.pose.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.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||||
self.ee1_calibrated = self.ee1_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
self.ee1_calibrated = self.ee1_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||||
self.ee2_calibrated = self.ee2_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
|
||||||
|
if self.face_detected and not self.driver_distracted:
|
||||||
|
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
|
||||||
|
if not standstill:
|
||||||
|
self.dcam_uncertain_cnt += 1
|
||||||
|
self.dcam_reset_cnt = 0
|
||||||
|
else:
|
||||||
|
self.dcam_reset_cnt += 1
|
||||||
|
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
|
||||||
|
self.dcam_uncertain_cnt = 0
|
||||||
|
|
||||||
self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME
|
self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME
|
||||||
self._set_timers(self.face_detected and not self.is_model_uncertain)
|
self._set_timers(self.face_detected and not self.is_model_uncertain)
|
||||||
@@ -376,6 +389,10 @@ class DriverMonitoring:
|
|||||||
if alert is not None:
|
if alert is not None:
|
||||||
self.current_events.add(alert)
|
self.current_events.add(alert)
|
||||||
|
|
||||||
|
if self.dcam_uncertain_cnt > self.settings._DCAM_UNCERTAIN_ALERT_COUNT and not self.dcam_uncertain_alerted:
|
||||||
|
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
|
||||||
|
self.dcam_uncertain_alerted = True
|
||||||
|
|
||||||
|
|
||||||
def get_state_packet(self, valid=True):
|
def get_state_packet(self, valid=True):
|
||||||
# build driverMonitoringState packet
|
# build driverMonitoringState packet
|
||||||
@@ -397,6 +414,7 @@ class DriverMonitoring:
|
|||||||
"hiStdCount": self.hi_stds,
|
"hiStdCount": self.hi_stds,
|
||||||
"isActiveMode": self.active_monitoring_mode,
|
"isActiveMode": self.active_monitoring_mode,
|
||||||
"isRHD": self.wheel_on_right,
|
"isRHD": self.wheel_on_right,
|
||||||
|
"uncertainCount": self.dcam_uncertain_cnt,
|
||||||
}
|
}
|
||||||
return dat
|
return dat
|
||||||
|
|
||||||
@@ -412,7 +430,8 @@ class DriverMonitoring:
|
|||||||
driver_state=sm['driverStateV2'],
|
driver_state=sm['driverStateV2'],
|
||||||
cal_rpy=sm['liveCalibration'].rpyCalib,
|
cal_rpy=sm['liveCalibration'].rpyCalib,
|
||||||
car_speed=sm['carState'].vEgo,
|
car_speed=sm['carState'].vEgo,
|
||||||
op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive
|
op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive,
|
||||||
|
standstill=sm['carState'].standstill,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update distraction events
|
# Update distraction events
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
|
|||||||
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
||||||
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
||||||
# TODO: test both separately when e2e is used
|
# TODO: test both separately when e2e is used
|
||||||
ds.leftDriverData.readyProb = [0., 0., 0., 0.]
|
|
||||||
ds.leftDriverData.notReadyProb = [0., 0.]
|
ds.leftDriverData.notReadyProb = [0., 0.]
|
||||||
return ds
|
return ds
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class TestMonitoring:
|
|||||||
DM = DriverMonitoring()
|
DM = DriverMonitoring()
|
||||||
events = []
|
events = []
|
||||||
for idx in range(len(msgs)):
|
for idx in range(len(msgs)):
|
||||||
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx])
|
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx])
|
||||||
# cal_rpy and car_speed don't matter here
|
# cal_rpy and car_speed don't matter here
|
||||||
|
|
||||||
# evaluate events at 10Hz for tests
|
# evaluate events at 10Hz for tests
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ Panda *connect(std::string serial="", uint32_t index=0) {
|
|||||||
}
|
}
|
||||||
//panda->enable_deepsleep();
|
//panda->enable_deepsleep();
|
||||||
|
|
||||||
for (int i = 0; i < PANDA_BUS_CNT; i++) {
|
for (int i = 0; i < PANDA_CAN_CNT; i++) {
|
||||||
panda->set_can_fd_auto(i, true);
|
panda->set_can_fd_auto(i, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import time
|
|||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from openpilot.common.gpio import gpio_set, gpio_init
|
from openpilot.common.gpio import gpio_set, gpio_init
|
||||||
from panda import Panda, PandaDFU, PandaProtocolMismatch
|
from panda import Panda, PandaDFU
|
||||||
from openpilot.common.retry import retry
|
|
||||||
from openpilot.system.manager.process_config import managed_processes
|
from openpilot.system.manager.process_config import managed_processes
|
||||||
from openpilot.system.hardware import HARDWARE
|
from openpilot.system.hardware import HARDWARE
|
||||||
from openpilot.system.hardware.tici.pins import GPIO
|
from openpilot.system.hardware.tici.pins import GPIO
|
||||||
@@ -50,8 +49,7 @@ class TestPandad:
|
|||||||
assert not Panda.wait_for_dfu(None, 3)
|
assert not Panda.wait_for_dfu(None, 3)
|
||||||
assert not Panda.wait_for_panda(None, 3)
|
assert not Panda.wait_for_panda(None, 3)
|
||||||
|
|
||||||
@retry(attempts=3)
|
def _flash_bootstub(self, fn):
|
||||||
def _flash_bootstub_and_test(self, fn, expect_mismatch=False):
|
|
||||||
self._go_to_dfu()
|
self._go_to_dfu()
|
||||||
pd = PandaDFU(None)
|
pd = PandaDFU(None)
|
||||||
if fn is None:
|
if fn is None:
|
||||||
@@ -61,16 +59,6 @@ class TestPandad:
|
|||||||
pd.reset()
|
pd.reset()
|
||||||
HARDWARE.reset_internal_panda()
|
HARDWARE.reset_internal_panda()
|
||||||
|
|
||||||
assert Panda.wait_for_panda(None, 10)
|
|
||||||
if expect_mismatch:
|
|
||||||
with pytest.raises(PandaProtocolMismatch):
|
|
||||||
Panda()
|
|
||||||
else:
|
|
||||||
with Panda() as p:
|
|
||||||
assert p.bootstub
|
|
||||||
|
|
||||||
self._run_test(45)
|
|
||||||
|
|
||||||
def test_in_dfu(self):
|
def test_in_dfu(self):
|
||||||
HARDWARE.recover_internal_panda()
|
HARDWARE.recover_internal_panda()
|
||||||
self._run_test(60)
|
self._run_test(60)
|
||||||
@@ -106,13 +94,14 @@ class TestPandad:
|
|||||||
print("startup times", ts, sum(ts) / len(ts))
|
print("startup times", ts, sum(ts) / len(ts))
|
||||||
assert 0.1 < (sum(ts)/len(ts)) < 0.7
|
assert 0.1 < (sum(ts)/len(ts)) < 0.7
|
||||||
|
|
||||||
def test_protocol_version_check(self):
|
def test_old_spi_protocol(self):
|
||||||
# flash old fw
|
# flash firmware with old SPI protocol
|
||||||
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")
|
self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin"))
|
||||||
self._flash_bootstub_and_test(fn, expect_mismatch=True)
|
self._run_test(45)
|
||||||
|
|
||||||
def test_release_to_devel_bootstub(self):
|
def test_release_to_devel_bootstub(self):
|
||||||
self._flash_bootstub_and_test(None)
|
self._flash_bootstub(None)
|
||||||
|
self._run_test(45)
|
||||||
|
|
||||||
def test_recover_from_bad_bootstub(self):
|
def test_recover_from_bad_bootstub(self):
|
||||||
self._go_to_dfu()
|
self._go_to_dfu()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pprint import pprint
|
|||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from cereal import car, log
|
from cereal import car, log
|
||||||
from opendbc.car.can_definitions import CanData
|
from opendbc.car.can_definitions import CanData
|
||||||
from openpilot.common.retry import retry
|
from openpilot.common.utils import retry
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.timeout import Timeout
|
from openpilot.common.timeout import Timeout
|
||||||
from openpilot.selfdrive.pandad import can_list_to_can_capnp
|
from openpilot.selfdrive.pandad import can_list_to_can_capnp
|
||||||
|
|||||||
@@ -41,6 +41,10 @@
|
|||||||
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
|
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
|
||||||
"severity": 0
|
"severity": 0
|
||||||
},
|
},
|
||||||
|
"Offroad_DriverMonitoringUncertain": {
|
||||||
|
"text": "openpilot detected poor visibility for driver monitoring. Ensure the device has a clear view of the driver. This can be checked using Settings -> Device -> Driver Camera Preview. Extreme lighting conditions and/or unconventional mounting positions may also trigger this alert.",
|
||||||
|
"severity": 0
|
||||||
|
},
|
||||||
"Offroad_ExcessiveActuation": {
|
"Offroad_ExcessiveActuation": {
|
||||||
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
|
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
|
||||||
"severity": 1,
|
"severity": 1,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.
|
|||||||
|
|
||||||
def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
||||||
return Alert(
|
return Alert(
|
||||||
f"Steer Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
|
f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
|
||||||
"",
|
"",
|
||||||
AlertStatus.userPrompt, AlertSize.small,
|
AlertStatus.userPrompt, AlertSize.small,
|
||||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
|
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
|
||||||
@@ -322,7 +322,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
|
|
||||||
EventName.steerTempUnavailableSilent: {
|
EventName.steerTempUnavailableSilent: {
|
||||||
ET.WARNING: Alert(
|
ET.WARNING: Alert(
|
||||||
"Steering Temporarily Unavailable",
|
"Steering Assist Temporarily Unavailable",
|
||||||
"",
|
"",
|
||||||
AlertStatus.userPrompt, AlertSize.small,
|
AlertStatus.userPrompt, AlertSize.small,
|
||||||
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
|
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
|
||||||
@@ -568,7 +568,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
EventName.steerTempUnavailable: {
|
EventName.steerTempUnavailable: {
|
||||||
ET.SOFT_DISABLE: soft_disable_alert("Steering Temporarily Unavailable"),
|
ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"),
|
||||||
ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"),
|
ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
afcab1abb62b9d5678342956cced4712f44e909e
|
b508f43fb0481bce0859c9b6ab4f45ee690b8dab
|
||||||
@@ -42,6 +42,7 @@ sudo systemctl restart NetworkManager
|
|||||||
sudo systemctl disable ssh-param-watcher.path
|
sudo systemctl disable ssh-param-watcher.path
|
||||||
sudo systemctl disable ssh-param-watcher.service
|
sudo systemctl disable ssh-param-watcher.service
|
||||||
sudo mount -o ro,remount /
|
sudo mount -o ro,remount /
|
||||||
|
sudo systemctl stop power_monitor
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
if ! sudo systemctl is-active -q ssh; then
|
if ! sudo systemctl is-active -q ssh; then
|
||||||
@@ -54,7 +55,6 @@ while true; do
|
|||||||
# /data/ciui.py &
|
# /data/ciui.py &
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog
|
|
||||||
sleep 5s
|
sleep 5s
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ CPU usage budget
|
|||||||
TEST_DURATION = 25
|
TEST_DURATION = 25
|
||||||
LOG_OFFSET = 8
|
LOG_OFFSET = 8
|
||||||
|
|
||||||
MAX_TOTAL_CPU = 300. # total for all 8 cores
|
MAX_TOTAL_CPU = 350. # total for all 8 cores
|
||||||
PROCS = {
|
PROCS = {
|
||||||
# Baseline CPU usage by process
|
# Baseline CPU usage by process
|
||||||
"selfdrive.controls.controlsd": 16.0,
|
"selfdrive.controls.controlsd": 16.0,
|
||||||
@@ -42,7 +42,7 @@ PROCS = {
|
|||||||
"./encoderd": 13.0,
|
"./encoderd": 13.0,
|
||||||
"./camerad": 10.0,
|
"./camerad": 10.0,
|
||||||
"selfdrive.controls.plannerd": 8.0,
|
"selfdrive.controls.plannerd": 8.0,
|
||||||
"./ui": 18.0,
|
"selfdrive.ui.ui": 40.0,
|
||||||
"system.sensord.sensord": 13.0,
|
"system.sensord.sensord": 13.0,
|
||||||
"selfdrive.controls.radard": 2.0,
|
"selfdrive.controls.radard": 2.0,
|
||||||
"selfdrive.modeld.modeld": 22.0,
|
"selfdrive.modeld.modeld": 22.0,
|
||||||
@@ -206,7 +206,8 @@ class TestOnroad:
|
|||||||
result += "-------------- UI Draw Timing ------------------\n"
|
result += "-------------- UI Draw Timing ------------------\n"
|
||||||
result += "------------------------------------------------\n"
|
result += "------------------------------------------------\n"
|
||||||
|
|
||||||
ts = self.ts['uiDebug']['drawTimeMillis']
|
# skip first few frames -- connecting to vipc
|
||||||
|
ts = self.ts['uiDebug']['drawTimeMillis'][15:]
|
||||||
result += f"min {min(ts):.2f}ms\n"
|
result += f"min {min(ts):.2f}ms\n"
|
||||||
result += f"max {max(ts):.2f}ms\n"
|
result += f"max {max(ts):.2f}ms\n"
|
||||||
result += f"std {np.std(ts):.2f}ms\n"
|
result += f"std {np.std(ts):.2f}ms\n"
|
||||||
@@ -215,7 +216,7 @@ class TestOnroad:
|
|||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
assert max(ts) < 250.
|
assert max(ts) < 250.
|
||||||
assert np.mean(ts) < 10.
|
assert np.mean(ts) < 20. # TODO: ~6-11ms, increase consistency
|
||||||
#self.assertLess(np.std(ts), 5.)
|
#self.assertLess(np.std(ts), 5.)
|
||||||
|
|
||||||
# some slow frames are expected since camerad/modeld can preempt ui
|
# some slow frames are expected since camerad/modeld can preempt ui
|
||||||
@@ -285,7 +286,7 @@ class TestOnroad:
|
|||||||
|
|
||||||
# check for big leaks. note that memory usage is
|
# check for big leaks. note that memory usage is
|
||||||
# expected to go up while the MSGQ buffers fill up
|
# expected to go up while the MSGQ buffers fill up
|
||||||
assert np.average(mems) <= 65, "Average memory usage above 65%"
|
assert np.average(mems) <= 85, "Average memory usage above 85%"
|
||||||
assert np.max(np.diff(mems)) <= 4, "Max memory increase too high"
|
assert np.max(np.diff(mems)) <= 4, "Max memory increase too high"
|
||||||
assert np.average(np.diff(mems)) <= 1, "Average memory increase too high"
|
assert np.average(np.diff(mems)) <= 1, "Average memory increase too high"
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1 @@
|
|||||||
moc_*
|
|
||||||
*.moc
|
|
||||||
|
|
||||||
translations/main_test_en.*
|
|
||||||
|
|
||||||
ui
|
|
||||||
mui
|
|
||||||
watch3
|
|
||||||
installer/installers/*
|
installer/installers/*
|
||||||
qt/setup/setup
|
|
||||||
qt/setup/reset
|
|
||||||
qt/setup/wifi
|
|
||||||
qt/setup/updater
|
|
||||||
translations/alerts_generated.h
|
|
||||||
|
|||||||
+27
-65
@@ -1,75 +1,37 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations')
|
from pathlib import Path
|
||||||
|
Import('env', 'arch', 'common')
|
||||||
|
|
||||||
base_libs = [common, messaging, visionipc, transformations,
|
# build the fonts
|
||||||
'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
generator = File("#selfdrive/assets/fonts/process.py")
|
||||||
|
source_files = Glob("#selfdrive/assets/fonts/*.ttf") + Glob("#selfdrive/assets/fonts/*.otf")
|
||||||
|
output_files = [
|
||||||
|
(f.abspath.split('.')[0] + ".fnt", f.abspath.split('.')[0] + ".png")
|
||||||
|
for f in source_files
|
||||||
|
if "NotoColor" not in f.name
|
||||||
|
]
|
||||||
|
env.Command(
|
||||||
|
target=output_files,
|
||||||
|
source=[generator, source_files],
|
||||||
|
action=f"python3 {generator}",
|
||||||
|
)
|
||||||
|
|
||||||
if arch == 'larch64':
|
# compile gettext .po -> .mo translations
|
||||||
base_libs.append('EGL')
|
|
||||||
|
|
||||||
if arch == "Darwin":
|
|
||||||
del base_libs[base_libs.index('OpenCL')]
|
|
||||||
qt_env['FRAMEWORKS'] += ['OpenCL']
|
|
||||||
|
|
||||||
sp_widgets_src = []
|
|
||||||
sp_qt_src = []
|
|
||||||
sp_qt_util = []
|
|
||||||
if not GetOption('stock_ui'):
|
|
||||||
SConscript(['sunnypilot/SConscript'])
|
|
||||||
Import('sp_widgets_src', 'sp_qt_src', 'sp_qt_util')
|
|
||||||
|
|
||||||
# FIXME: remove this once we're on 5.15 (24.04)
|
|
||||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
|
||||||
|
|
||||||
qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"] + sp_qt_util, LIBS=base_libs)
|
|
||||||
widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc",
|
|
||||||
"qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc",
|
|
||||||
"qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc",
|
|
||||||
"qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc",
|
|
||||||
"qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src
|
|
||||||
|
|
||||||
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
|
|
||||||
Export('widgets')
|
|
||||||
qt_libs = [widgets, qt_util] + base_libs
|
|
||||||
|
|
||||||
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
|
|
||||||
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/offroad_home.cc",
|
|
||||||
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc",
|
|
||||||
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",
|
|
||||||
"qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc",
|
|
||||||
"qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src
|
|
||||||
|
|
||||||
# build translation files
|
|
||||||
with open(File("translations/languages.json").abspath) as f:
|
with open(File("translations/languages.json").abspath) as f:
|
||||||
languages = json.loads(f.read())
|
languages = json.loads(f.read())
|
||||||
translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()]
|
|
||||||
translation_targets = [src.replace(".ts", ".qm") for src in translation_sources]
|
|
||||||
lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease'
|
|
||||||
|
|
||||||
lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES")
|
po_sources = [f"#selfdrive/ui/translations/app_{l}.po" for l in languages.values()]
|
||||||
qt_env.NoClean(translation_sources)
|
po_sources = [src for src in po_sources if os.path.exists(File(src).abspath)]
|
||||||
qt_env.Precious(translation_sources)
|
mo_targets = [src.replace(".po", ".mo") for src in po_sources]
|
||||||
|
mo_build = []
|
||||||
|
for src, tgt in zip(po_sources, mo_targets):
|
||||||
|
mo_build.append(env.Command(tgt, src, "msgfmt -o $TARGET $SOURCE"))
|
||||||
|
mo_alias = env.Alias('mo', mo_build)
|
||||||
|
env.AlwaysBuild(mo_alias)
|
||||||
|
|
||||||
# create qrc file for compiled translations to include with assets
|
|
||||||
translations_assets_src = "#selfdrive/assets/translations_assets.qrc"
|
|
||||||
with open(File(translations_assets_src).abspath, 'w') as f:
|
|
||||||
f.write('<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n')
|
|
||||||
f.write('\n'.join([f'<file alias="{l}">../ui/translations/{l}.qm</file>' for l in languages.values()]))
|
|
||||||
f.write('\n</qresource>\n</RCC>')
|
|
||||||
|
|
||||||
# build assets
|
|
||||||
assets = "#selfdrive/assets/assets.cc"
|
|
||||||
assets_src = "#selfdrive/assets/assets.qrc"
|
|
||||||
qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET")
|
|
||||||
qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease])
|
|
||||||
asset_obj = qt_env.Object("assets", assets)
|
|
||||||
|
|
||||||
# build main UI
|
|
||||||
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs)
|
|
||||||
if GetOption('extras'):
|
if GetOption('extras'):
|
||||||
qt_src.remove("main.cc") # replaced by test_runner
|
|
||||||
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
|
|
||||||
|
|
||||||
# build installers
|
# build installers
|
||||||
if arch != "Darwin":
|
if arch != "Darwin":
|
||||||
raylib_env = env.Clone()
|
raylib_env = env.Clone()
|
||||||
@@ -78,7 +40,7 @@ if GetOption('extras'):
|
|||||||
|
|
||||||
raylib_libs = common + ["raylib"]
|
raylib_libs = common + ["raylib"]
|
||||||
if arch == "larch64":
|
if arch == "larch64":
|
||||||
raylib_libs += ["GLESv2", "wayland-client", "wayland-egl", "EGL"]
|
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
|
||||||
else:
|
else:
|
||||||
raylib_libs += ["GL"]
|
raylib_libs += ["GL"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
UI_BORDER_SIZE = 30
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "common/swaglog.h"
|
#include "common/swaglog.h"
|
||||||
#include "common/util.h"
|
#include "common/util.h"
|
||||||
|
#include "system/hardware/hw.h"
|
||||||
#include "third_party/raylib/include/raylib.h"
|
#include "third_party/raylib/include/raylib.h"
|
||||||
|
|
||||||
int freshClone();
|
int freshClone();
|
||||||
@@ -38,6 +39,27 @@ extern const uint8_t inter_ttf_end[] asm("_binary_selfdrive_ui_installer_inter_a
|
|||||||
|
|
||||||
Font font;
|
Font font;
|
||||||
|
|
||||||
|
std::vector<std::string> tici_prebuilt_branches = {"release3", "release-tizi", "release3-staging", "nightly", "nightly-dev"};
|
||||||
|
std::string migrated_branch;
|
||||||
|
|
||||||
|
void branchMigration() {
|
||||||
|
migrated_branch = BRANCH_STR;
|
||||||
|
cereal::InitData::DeviceType device_type = Hardware::get_device_type();
|
||||||
|
if (device_type == cereal::InitData::DeviceType::TICI) {
|
||||||
|
if (std::find(tici_prebuilt_branches.begin(), tici_prebuilt_branches.end(), BRANCH_STR) != tici_prebuilt_branches.end()) {
|
||||||
|
migrated_branch = "release-tici";
|
||||||
|
} else if (BRANCH_STR == "master") {
|
||||||
|
migrated_branch = "master-tici";
|
||||||
|
}
|
||||||
|
} else if (device_type == cereal::InitData::DeviceType::TIZI) {
|
||||||
|
if (BRANCH_STR == "release3") {
|
||||||
|
migrated_branch = "release-tizi";
|
||||||
|
} else if (BRANCH_STR == "release3-staging") {
|
||||||
|
migrated_branch = "release-tizi-staging";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void run(const char* cmd) {
|
void run(const char* cmd) {
|
||||||
int err = std::system(cmd);
|
int err = std::system(cmd);
|
||||||
assert(err == 0);
|
assert(err == 0);
|
||||||
@@ -87,7 +109,7 @@ int doInstall() {
|
|||||||
int freshClone() {
|
int freshClone() {
|
||||||
LOGD("Doing fresh clone");
|
LOGD("Doing fresh clone");
|
||||||
std::string cmd = util::string_format("git clone --progress %s -b %s --depth=1 --recurse-submodules %s 2>&1",
|
std::string cmd = util::string_format("git clone --progress %s -b %s --depth=1 --recurse-submodules %s 2>&1",
|
||||||
GIT_URL.c_str(), BRANCH_STR.c_str(), TMP_INSTALL_PATH);
|
GIT_URL.c_str(), migrated_branch.c_str(), TMP_INSTALL_PATH);
|
||||||
return executeGitCommand(cmd);
|
return executeGitCommand(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +117,11 @@ int cachedFetch(const std::string &cache) {
|
|||||||
LOGD("Fetching with cache: %s", cache.c_str());
|
LOGD("Fetching with cache: %s", cache.c_str());
|
||||||
|
|
||||||
run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str());
|
run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str());
|
||||||
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, BRANCH_STR.c_str()).c_str());
|
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, migrated_branch.c_str()).c_str());
|
||||||
|
|
||||||
renderProgress(10);
|
renderProgress(10);
|
||||||
|
|
||||||
return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, BRANCH_STR.c_str()));
|
return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, migrated_branch.c_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
int executeGitCommand(const std::string &cmd) {
|
int executeGitCommand(const std::string &cmd) {
|
||||||
@@ -142,8 +164,8 @@ void cloneFinished(int exitCode) {
|
|||||||
// ensure correct branch is checked out
|
// ensure correct branch is checked out
|
||||||
int err = chdir(TMP_INSTALL_PATH);
|
int err = chdir(TMP_INSTALL_PATH);
|
||||||
assert(err == 0);
|
assert(err == 0);
|
||||||
run(("git checkout " + BRANCH_STR).c_str());
|
run(("git checkout " + migrated_branch).c_str());
|
||||||
run(("git reset --hard origin/" + BRANCH_STR).c_str());
|
run(("git reset --hard origin/" + migrated_branch).c_str());
|
||||||
run("git submodule update --init");
|
run("git submodule update --init");
|
||||||
|
|
||||||
// move into place
|
// move into place
|
||||||
@@ -193,6 +215,8 @@ int main(int argc, char *argv[]) {
|
|||||||
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0);
|
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0);
|
||||||
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
|
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
|
||||||
|
|
||||||
|
branchMigration();
|
||||||
|
|
||||||
if (util::file_exists(CONTINUE_PATH)) {
|
if (util::file_exists(CONTINUE_PATH)) {
|
||||||
finishInstall();
|
finishInstall();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
|
|||||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, trn
|
||||||
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
|
||||||
HEADER_HEIGHT = 80
|
HEADER_HEIGHT = 80
|
||||||
@@ -35,12 +37,17 @@ class HomeLayout(Widget):
|
|||||||
self.update_alert = UpdateAlert()
|
self.update_alert = UpdateAlert()
|
||||||
self.offroad_alert = OffroadAlert()
|
self.offroad_alert = OffroadAlert()
|
||||||
|
|
||||||
|
self._layout_widgets = {HomeLayoutState.UPDATE: self.update_alert, HomeLayoutState.ALERTS: self.offroad_alert}
|
||||||
|
|
||||||
self.current_state = HomeLayoutState.HOME
|
self.current_state = HomeLayoutState.HOME
|
||||||
self.last_refresh = 0
|
self.last_refresh = 0
|
||||||
self.settings_callback: callable | None = None
|
self.settings_callback: callable | None = None
|
||||||
|
|
||||||
self.update_available = False
|
self.update_available = False
|
||||||
self.alert_count = 0
|
self.alert_count = 0
|
||||||
|
self._version_text = ""
|
||||||
|
self._prev_update_available = False
|
||||||
|
self._prev_alerts_present = False
|
||||||
|
|
||||||
self.header_rect = rl.Rectangle(0, 0, 0, 0)
|
self.header_rect = rl.Rectangle(0, 0, 0, 0)
|
||||||
self.content_rect = rl.Rectangle(0, 0, 0, 0)
|
self.content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||||
@@ -56,14 +63,30 @@ class HomeLayout(Widget):
|
|||||||
self._exp_mode_button = ExperimentalModeButton()
|
self._exp_mode_button = ExperimentalModeButton()
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
self._exp_mode_button.show_event()
|
||||||
|
self.last_refresh = time.monotonic()
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||||
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||||
|
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
|
||||||
|
|
||||||
def set_settings_callback(self, callback: Callable):
|
def set_settings_callback(self, callback: Callable):
|
||||||
self.settings_callback = callback
|
self.settings_callback = callback
|
||||||
|
|
||||||
def _set_state(self, state: HomeLayoutState):
|
def _set_state(self, state: HomeLayoutState):
|
||||||
|
# propagate show/hide events
|
||||||
|
if state != self.current_state:
|
||||||
|
if state == HomeLayoutState.HOME:
|
||||||
|
self._exp_mode_button.show_event()
|
||||||
|
|
||||||
|
if state in self._layout_widgets:
|
||||||
|
self._layout_widgets[state].show_event()
|
||||||
|
if self.current_state in self._layout_widgets:
|
||||||
|
self._layout_widgets[self.current_state].hide_event()
|
||||||
|
|
||||||
self.current_state = state
|
self.current_state = state
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
@@ -72,7 +95,6 @@ class HomeLayout(Widget):
|
|||||||
self._refresh()
|
self._refresh()
|
||||||
self.last_refresh = current_time
|
self.last_refresh = current_time
|
||||||
|
|
||||||
self._handle_input()
|
|
||||||
self._render_header()
|
self._render_header()
|
||||||
|
|
||||||
# Render content based on current state
|
# Render content based on current state
|
||||||
@@ -83,7 +105,7 @@ class HomeLayout(Widget):
|
|||||||
elif self.current_state == HomeLayoutState.ALERTS:
|
elif self.current_state == HomeLayoutState.ALERTS:
|
||||||
self._render_alerts_view()
|
self._render_alerts_view()
|
||||||
|
|
||||||
def _update_layout_rects(self):
|
def _update_state(self):
|
||||||
self.header_rect = rl.Rectangle(
|
self.header_rect = rl.Rectangle(
|
||||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||||
)
|
)
|
||||||
@@ -110,59 +132,54 @@ class HomeLayout(Widget):
|
|||||||
self.alert_notif_rect.x = notif_x
|
self.alert_notif_rect.x = notif_x
|
||||||
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||||
|
|
||||||
def _handle_input(self):
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||||
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
super()._handle_mouse_release(mouse_pos)
|
||||||
return
|
|
||||||
|
|
||||||
mouse_pos = rl.get_mouse_position()
|
|
||||||
|
|
||||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
|
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
|
||||||
self._set_state(HomeLayoutState.UPDATE)
|
self._set_state(HomeLayoutState.UPDATE)
|
||||||
return
|
elif self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
||||||
|
|
||||||
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
|
||||||
self._set_state(HomeLayoutState.ALERTS)
|
self._set_state(HomeLayoutState.ALERTS)
|
||||||
return
|
|
||||||
|
|
||||||
# Content area input handling
|
|
||||||
if self.current_state == HomeLayoutState.UPDATE:
|
|
||||||
self.update_alert.handle_input(mouse_pos, True)
|
|
||||||
elif self.current_state == HomeLayoutState.ALERTS:
|
|
||||||
self.offroad_alert.handle_input(mouse_pos, True)
|
|
||||||
|
|
||||||
def _render_header(self):
|
def _render_header(self):
|
||||||
font = gui_app.font(FontWeight.MEDIUM)
|
font = gui_app.font(FontWeight.MEDIUM)
|
||||||
|
|
||||||
|
version_text_width = self.header_rect.width
|
||||||
|
|
||||||
# Update notification button
|
# Update notification button
|
||||||
if self.update_available:
|
if self.update_available:
|
||||||
|
version_text_width -= self.update_notif_rect.width
|
||||||
|
|
||||||
# Highlight if currently viewing updates
|
# Highlight if currently viewing updates
|
||||||
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255)
|
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
|
||||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
||||||
|
|
||||||
text = "UPDATE"
|
text = tr("UPDATE")
|
||||||
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
|
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
|
||||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
|
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
|
||||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
|
||||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||||
|
|
||||||
# Alert notification button
|
# Alert notification button
|
||||||
if self.alert_count > 0:
|
if self.alert_count > 0:
|
||||||
|
version_text_width -= self.alert_notif_rect.width
|
||||||
|
|
||||||
# Highlight if currently viewing alerts
|
# Highlight if currently viewing alerts
|
||||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
||||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
||||||
|
|
||||||
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
|
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
|
||||||
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
|
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
|
||||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
|
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
|
||||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
|
||||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||||
|
|
||||||
# Version text (right aligned)
|
# Version text (right aligned)
|
||||||
version_text = self._get_version_text()
|
if self.update_available or self.alert_count > 0:
|
||||||
text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x
|
version_text_width -= SPACING * 1.5
|
||||||
version_x = self.header_rect.x + self.header_rect.width - text_width
|
|
||||||
version_y = self.header_rect.y + (self.header_rect.height - 48) // 2
|
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR)
|
version_text_width, self.header_rect.height)
|
||||||
|
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
|
|
||||||
def _render_home_content(self):
|
def _render_home_content(self):
|
||||||
self._render_left_column()
|
self._render_left_column()
|
||||||
@@ -193,20 +210,23 @@ class HomeLayout(Widget):
|
|||||||
self._setup_widget.render(setup_rect)
|
self._setup_widget.render(setup_rect)
|
||||||
|
|
||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
# TODO: implement _update_state with a timer
|
self._version_text = self._get_version_text()
|
||||||
self.update_available = self.update_alert.refresh()
|
update_available = self.update_alert.refresh()
|
||||||
self.alert_count = self.offroad_alert.refresh()
|
alert_count = self.offroad_alert.refresh()
|
||||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
alerts_present = alert_count > 0
|
||||||
|
|
||||||
def _update_state_priority(self, update_available: bool, alerts_present: bool):
|
|
||||||
current_state = self.current_state
|
|
||||||
|
|
||||||
|
# Show panels on transition from no alert/update to any alerts/update
|
||||||
if not update_available and not alerts_present:
|
if not update_available and not alerts_present:
|
||||||
self.current_state = HomeLayoutState.HOME
|
self._set_state(HomeLayoutState.HOME)
|
||||||
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)):
|
elif update_available and ((not self._prev_update_available) or (not alerts_present and self.current_state == HomeLayoutState.ALERTS)):
|
||||||
self.current_state = HomeLayoutState.UPDATE
|
self._set_state(HomeLayoutState.UPDATE)
|
||||||
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)):
|
elif alerts_present and ((not self._prev_alerts_present) or (not update_available and self.current_state == HomeLayoutState.UPDATE)):
|
||||||
self.current_state = HomeLayoutState.ALERTS
|
self._set_state(HomeLayoutState.ALERTS)
|
||||||
|
|
||||||
|
self.update_available = update_available
|
||||||
|
self.alert_count = alert_count
|
||||||
|
self._prev_update_available = update_available
|
||||||
|
self._prev_alerts_present = alerts_present
|
||||||
|
|
||||||
def _get_version_text(self) -> str:
|
def _get_version_text(self) -> str:
|
||||||
brand = "openpilot"
|
brand = "openpilot"
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
||||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
if Params().get_bool("sunnypilot_ui"):
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||||
|
|
||||||
class MainState(IntEnum):
|
class MainState(IntEnum):
|
||||||
HOME = 0
|
HOME = 0
|
||||||
@@ -34,16 +39,23 @@ class MainLayout(Widget):
|
|||||||
# Set callbacks
|
# Set callbacks
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
|
# Start onboarding if terms or training not completed
|
||||||
|
self._onboarding_window = OnboardingWindow()
|
||||||
|
if not self._onboarding_window.completed:
|
||||||
|
gui_app.set_modal_overlay(self._onboarding_window)
|
||||||
|
|
||||||
def _render(self, _):
|
def _render(self, _):
|
||||||
self._handle_onroad_transition()
|
self._handle_onroad_transition()
|
||||||
self._render_main_content()
|
self._render_main_content()
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
||||||
on_flag=self._on_bookmark_clicked)
|
on_flag=self._on_bookmark_clicked,
|
||||||
|
open_settings=lambda: self.open_settings(PanelType.TOGGLES))
|
||||||
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
||||||
|
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
|
||||||
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
||||||
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
|
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
|
||||||
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
||||||
|
|
||||||
def _update_layout_rects(self):
|
def _update_layout_rects(self):
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
import pyray as rl
|
||||||
|
from openpilot.common.basedir import BASEDIR
|
||||||
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
|
from openpilot.system.ui.widgets.label import Label
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
STEP_RECTS = [rl.Rectangle(104, 800, 633, 175), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2156, 1080),
|
||||||
|
rl.Rectangle(1526, 473, 427, 472), rl.Rectangle(1643, 441, 217, 223), rl.Rectangle(1835, 0, 2155, 1080),
|
||||||
|
rl.Rectangle(1786, 591, 267, 236), rl.Rectangle(1353, 0, 804, 1080), rl.Rectangle(1458, 485, 633, 211),
|
||||||
|
rl.Rectangle(95, 794, 1158, 187), rl.Rectangle(1560, 170, 392, 397), rl.Rectangle(1835, 0, 2159, 1080),
|
||||||
|
rl.Rectangle(1351, 0, 807, 1080), rl.Rectangle(1835, 0, 2158, 1080), rl.Rectangle(1531, 82, 441, 920),
|
||||||
|
rl.Rectangle(1336, 438, 490, 393), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2159, 1080),
|
||||||
|
rl.Rectangle(87, 795, 1187, 186)]
|
||||||
|
|
||||||
|
DM_RECORD_STEP = 9
|
||||||
|
DM_RECORD_YES_RECT = rl.Rectangle(695, 794, 558, 187)
|
||||||
|
|
||||||
|
RESTART_TRAINING_RECT = rl.Rectangle(87, 795, 472, 186)
|
||||||
|
|
||||||
|
|
||||||
|
class OnboardingState(IntEnum):
|
||||||
|
TERMS = 0
|
||||||
|
ONBOARDING = 1
|
||||||
|
DECLINE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class TrainingGuide(Widget):
|
||||||
|
def __init__(self, completed_callback=None):
|
||||||
|
super().__init__()
|
||||||
|
self._completed_callback = completed_callback
|
||||||
|
|
||||||
|
self._step = 0
|
||||||
|
self._load_image_paths()
|
||||||
|
|
||||||
|
# Load first image now so we show something immediately
|
||||||
|
self._textures = [gui_app.texture(self._image_paths[0])]
|
||||||
|
self._image_objs = []
|
||||||
|
|
||||||
|
threading.Thread(target=self._preload_thread, daemon=True).start()
|
||||||
|
|
||||||
|
def _load_image_paths(self):
|
||||||
|
paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)]
|
||||||
|
paths = sorted(paths, key=lambda x: int(re.search(r'\d+', x).group()))
|
||||||
|
self._image_paths = [os.path.join(BASEDIR, "selfdrive/assets/training", fn) for fn in paths]
|
||||||
|
|
||||||
|
def _preload_thread(self):
|
||||||
|
# PNG loading is slow in raylib, so we preload in a thread and upload to GPU in main thread
|
||||||
|
# We've already loaded the first image on init
|
||||||
|
for path in self._image_paths[1:]:
|
||||||
|
self._image_objs.append(gui_app._load_image_from_path(path))
|
||||||
|
|
||||||
|
def _handle_mouse_release(self, mouse_pos):
|
||||||
|
if rl.check_collision_point_rec(mouse_pos, STEP_RECTS[self._step]):
|
||||||
|
# Record DM camera?
|
||||||
|
if self._step == DM_RECORD_STEP:
|
||||||
|
yes = rl.check_collision_point_rec(mouse_pos, DM_RECORD_YES_RECT)
|
||||||
|
print(f"putting RecordFront to {yes}")
|
||||||
|
ui_state.params.put_bool("RecordFront", yes)
|
||||||
|
|
||||||
|
# Restart training?
|
||||||
|
elif self._step == len(self._image_paths) - 1:
|
||||||
|
if rl.check_collision_point_rec(mouse_pos, RESTART_TRAINING_RECT):
|
||||||
|
self._step = -1
|
||||||
|
|
||||||
|
self._step += 1
|
||||||
|
|
||||||
|
# Finished?
|
||||||
|
if self._step >= len(self._image_paths):
|
||||||
|
self._step = 0
|
||||||
|
if self._completed_callback:
|
||||||
|
self._completed_callback()
|
||||||
|
|
||||||
|
def _update_state(self):
|
||||||
|
if len(self._image_objs):
|
||||||
|
self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0)))
|
||||||
|
|
||||||
|
def _render(self, _):
|
||||||
|
# Safeguard against fast tapping
|
||||||
|
step = min(self._step, len(self._textures) - 1)
|
||||||
|
rl.draw_texture(self._textures[step], 0, 0, rl.WHITE)
|
||||||
|
|
||||||
|
# progress bar
|
||||||
|
if 0 < step < len(STEP_RECTS) - 1:
|
||||||
|
h = 20
|
||||||
|
w = int((step / (len(STEP_RECTS) - 1)) * self._rect.width)
|
||||||
|
rl.draw_rectangle(int(self._rect.x), int(self._rect.y + self._rect.height - h),
|
||||||
|
w, h, rl.Color(70, 91, 234, 255))
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
rl.draw_rectangle_lines_ex(STEP_RECTS[step], 3, rl.RED)
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
class TermsPage(Widget):
|
||||||
|
def __init__(self, on_accept=None, on_decline=None):
|
||||||
|
super().__init__()
|
||||||
|
self._on_accept = on_accept
|
||||||
|
self._on_decline = on_decline
|
||||||
|
|
||||||
|
self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
|
self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."),
|
||||||
|
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
|
|
||||||
|
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||||
|
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||||
|
|
||||||
|
def _render(self, _):
|
||||||
|
welcome_x = self._rect.x + 165
|
||||||
|
welcome_y = self._rect.y + 165
|
||||||
|
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
|
||||||
|
self._title.render(welcome_rect)
|
||||||
|
|
||||||
|
desc_x = welcome_x
|
||||||
|
# TODO: Label doesn't top align when wrapping
|
||||||
|
desc_y = welcome_y - 100
|
||||||
|
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
|
||||||
|
self._desc.render(desc_rect)
|
||||||
|
|
||||||
|
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||||
|
btn_width = (self._rect.width - 45 * 3) / 2
|
||||||
|
self._decline_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||||
|
self._accept_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
rl.draw_rectangle_lines_ex(welcome_rect, 3, rl.RED)
|
||||||
|
rl.draw_rectangle_lines_ex(desc_rect, 3, rl.RED)
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
class DeclinePage(Widget):
|
||||||
|
def __init__(self, back_callback=None):
|
||||||
|
super().__init__()
|
||||||
|
self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."),
|
||||||
|
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
|
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||||
|
self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
|
||||||
|
click_callback=self._on_uninstall_clicked)
|
||||||
|
|
||||||
|
def _on_uninstall_clicked(self):
|
||||||
|
ui_state.params.put_bool("DoUninstall", True)
|
||||||
|
gui_app.request_close()
|
||||||
|
|
||||||
|
def _render(self, _):
|
||||||
|
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||||
|
btn_width = (self._rect.width - 45 * 3) / 2
|
||||||
|
self._back_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||||
|
self._uninstall_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||||
|
|
||||||
|
# text rect in middle of top and button
|
||||||
|
text_height = btn_y - (200 + 45)
|
||||||
|
text_rect = rl.Rectangle(self._rect.x + 165, self._rect.y + (btn_y - text_height) / 2 + 10, self._rect.width - (165 * 2), text_height)
|
||||||
|
if DEBUG:
|
||||||
|
rl.draw_rectangle_lines_ex(text_rect, 3, rl.RED)
|
||||||
|
self._text.render(text_rect)
|
||||||
|
|
||||||
|
|
||||||
|
class OnboardingWindow(Widget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._current_terms_version = ui_state.params.get("TermsVersion")
|
||||||
|
self._current_training_version = ui_state.params.get("TrainingVersion")
|
||||||
|
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == self._current_terms_version
|
||||||
|
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == self._current_training_version
|
||||||
|
|
||||||
|
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
|
||||||
|
self._training_guide: TrainingGuide | None = None
|
||||||
|
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed(self) -> bool:
|
||||||
|
return self._accepted_terms and self._training_done
|
||||||
|
|
||||||
|
def _on_terms_declined(self):
|
||||||
|
self._state = OnboardingState.DECLINE
|
||||||
|
|
||||||
|
def _on_decline_back(self):
|
||||||
|
self._state = OnboardingState.TERMS
|
||||||
|
|
||||||
|
def _on_terms_accepted(self):
|
||||||
|
ui_state.params.put("HasAcceptedTerms", self._current_terms_version)
|
||||||
|
self._state = OnboardingState.ONBOARDING
|
||||||
|
if self._training_done:
|
||||||
|
gui_app.set_modal_overlay(None)
|
||||||
|
|
||||||
|
def _on_completed_training(self):
|
||||||
|
ui_state.params.put("CompletedTrainingVersion", self._current_training_version)
|
||||||
|
gui_app.set_modal_overlay(None)
|
||||||
|
|
||||||
|
def _render(self, _):
|
||||||
|
if self._training_guide is None:
|
||||||
|
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||||
|
|
||||||
|
if self._state == OnboardingState.TERMS:
|
||||||
|
self._terms.render(self._rect)
|
||||||
|
if self._state == OnboardingState.ONBOARDING:
|
||||||
|
self._training_guide.render(self._rect)
|
||||||
|
elif self._state == OnboardingState.DECLINE:
|
||||||
|
self._decline_page.render(self._rect)
|
||||||
|
return -1
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.list_view import toggle_item
|
from openpilot.system.ui.widgets.list_view import toggle_item
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
|
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||||
|
from openpilot.system.ui.widgets import DialogResult
|
||||||
|
|
||||||
|
if Params().get_bool("sunnypilot_ui"):
|
||||||
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
'enable_adb': (
|
'enable_adb': tr_noop(
|
||||||
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
|
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
|
||||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
||||||
),
|
),
|
||||||
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
'ssh_key': tr_noop(
|
||||||
'ssh_key': (
|
|
||||||
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||||
),
|
),
|
||||||
|
'alpha_longitudinal': tr_noop(
|
||||||
|
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
|
||||||
|
"On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " +
|
||||||
|
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. " +
|
||||||
|
"Changing this setting will restart openpilot if the car is powered on."
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -22,40 +35,154 @@ class DeveloperLayout(Widget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
items = [
|
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||||
toggle_item(
|
|
||||||
"Enable ADB",
|
|
||||||
description=DESCRIPTIONS["enable_adb"],
|
|
||||||
initial_state=self._params.get_bool("AdbEnabled"),
|
|
||||||
callback=self._on_enable_adb,
|
|
||||||
),
|
|
||||||
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
|
||||||
toggle_item(
|
|
||||||
"Joystick Debug Mode",
|
|
||||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
|
||||||
initial_state=self._params.get_bool("JoystickDebugMode"),
|
|
||||||
callback=self._on_joystick_debug_mode,
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Longitudinal Maneuver Mode",
|
|
||||||
description="",
|
|
||||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
|
||||||
callback=self._on_long_maneuver_mode,
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"openpilot Longitudinal Control (Alpha)",
|
|
||||||
description="",
|
|
||||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
|
||||||
callback=self._on_alpha_long_enabled,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
# Build items and keep references for callbacks/state updates
|
||||||
|
self._adb_toggle = toggle_item(
|
||||||
|
lambda: tr("Enable ADB"),
|
||||||
|
description=lambda: tr(DESCRIPTIONS["enable_adb"]),
|
||||||
|
initial_state=self._params.get_bool("AdbEnabled"),
|
||||||
|
callback=self._on_enable_adb,
|
||||||
|
enabled=ui_state.is_offroad,
|
||||||
|
)
|
||||||
|
|
||||||
|
# SSH enable toggle + SSH key management
|
||||||
|
self._ssh_toggle = toggle_item(
|
||||||
|
lambda: tr("Enable SSH"),
|
||||||
|
description="",
|
||||||
|
initial_state=self._params.get_bool("SshEnabled"),
|
||||||
|
callback=self._on_enable_ssh,
|
||||||
|
)
|
||||||
|
self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: tr(DESCRIPTIONS["ssh_key"]))
|
||||||
|
|
||||||
|
self._joystick_toggle = toggle_item(
|
||||||
|
lambda: tr("Joystick Debug Mode"),
|
||||||
|
description="",
|
||||||
|
initial_state=self._params.get_bool("JoystickDebugMode"),
|
||||||
|
callback=self._on_joystick_debug_mode,
|
||||||
|
enabled=ui_state.is_offroad,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._long_maneuver_toggle = toggle_item(
|
||||||
|
lambda: tr("Longitudinal Maneuver Mode"),
|
||||||
|
description="",
|
||||||
|
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||||
|
callback=self._on_long_maneuver_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._alpha_long_toggle = toggle_item(
|
||||||
|
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||||
|
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
||||||
|
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||||
|
callback=self._on_alpha_long_enabled,
|
||||||
|
enabled=lambda: not ui_state.engaged,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._ui_debug_toggle = toggle_item(
|
||||||
|
lambda: tr("UI Debug Mode"),
|
||||||
|
description="",
|
||||||
|
initial_state=self._params.get_bool("ShowDebugInfo"),
|
||||||
|
callback=self._on_enable_ui_debug,
|
||||||
|
)
|
||||||
|
self._on_enable_ui_debug(self._params.get_bool("ShowDebugInfo"))
|
||||||
|
|
||||||
|
self._scroller = Scroller([
|
||||||
|
self._adb_toggle,
|
||||||
|
self._ssh_toggle,
|
||||||
|
self._ssh_keys,
|
||||||
|
self._joystick_toggle,
|
||||||
|
self._long_maneuver_toggle,
|
||||||
|
self._alpha_long_toggle,
|
||||||
|
self._ui_debug_toggle,
|
||||||
|
], line_separator=True, spacing=0)
|
||||||
|
|
||||||
|
# Toggles should be not available to change in onroad state
|
||||||
|
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
self._scroller.render(rect)
|
self._scroller.render(rect)
|
||||||
|
|
||||||
def _on_enable_adb(self): pass
|
def show_event(self):
|
||||||
def _on_joystick_debug_mode(self): pass
|
self._scroller.show_event()
|
||||||
def _on_long_maneuver_mode(self): pass
|
self._update_toggles()
|
||||||
def _on_alpha_long_enabled(self): pass
|
|
||||||
|
def _update_toggles(self):
|
||||||
|
ui_state.update_params()
|
||||||
|
|
||||||
|
# Hide non-release toggles on release builds
|
||||||
|
# TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault
|
||||||
|
for item in (self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle):
|
||||||
|
item.set_visible(not self._is_release)
|
||||||
|
|
||||||
|
# CP gating
|
||||||
|
if ui_state.CP is not None:
|
||||||
|
alpha_avail = ui_state.CP.alphaLongitudinalAvailable
|
||||||
|
if not alpha_avail or self._is_release:
|
||||||
|
self._alpha_long_toggle.set_visible(False)
|
||||||
|
self._params.remove("AlphaLongitudinalEnabled")
|
||||||
|
else:
|
||||||
|
self._alpha_long_toggle.set_visible(True)
|
||||||
|
|
||||||
|
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||||
|
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
|
||||||
|
if not long_man_enabled:
|
||||||
|
self._long_maneuver_toggle.action_item.set_state(False)
|
||||||
|
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||||
|
else:
|
||||||
|
self._long_maneuver_toggle.action_item.set_enabled(False)
|
||||||
|
self._alpha_long_toggle.set_visible(False)
|
||||||
|
|
||||||
|
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||||
|
# refresh toggles from params to mirror external changes
|
||||||
|
for key, item in (
|
||||||
|
("AdbEnabled", self._adb_toggle),
|
||||||
|
("SshEnabled", self._ssh_toggle),
|
||||||
|
("JoystickDebugMode", self._joystick_toggle),
|
||||||
|
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||||
|
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
||||||
|
("ShowDebugInfo", self._ui_debug_toggle),
|
||||||
|
):
|
||||||
|
item.action_item.set_state(self._params.get_bool(key))
|
||||||
|
|
||||||
|
def _on_enable_ui_debug(self, state: bool):
|
||||||
|
self._params.put_bool("ShowDebugInfo", state)
|
||||||
|
gui_app.set_show_touches(state)
|
||||||
|
gui_app.set_show_fps(state)
|
||||||
|
|
||||||
|
def _on_enable_adb(self, state: bool):
|
||||||
|
self._params.put_bool("AdbEnabled", state)
|
||||||
|
|
||||||
|
def _on_enable_ssh(self, state: bool):
|
||||||
|
self._params.put_bool("SshEnabled", state)
|
||||||
|
|
||||||
|
def _on_joystick_debug_mode(self, state: bool):
|
||||||
|
self._params.put_bool("JoystickDebugMode", state)
|
||||||
|
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||||
|
self._long_maneuver_toggle.action_item.set_state(False)
|
||||||
|
|
||||||
|
def _on_long_maneuver_mode(self, state: bool):
|
||||||
|
self._params.put_bool("LongitudinalManeuverMode", state)
|
||||||
|
self._params.put_bool("JoystickDebugMode", False)
|
||||||
|
self._joystick_toggle.action_item.set_state(False)
|
||||||
|
|
||||||
|
def _on_alpha_long_enabled(self, state: bool):
|
||||||
|
if state:
|
||||||
|
def confirm_callback(result: int):
|
||||||
|
if result == DialogResult.CONFIRM:
|
||||||
|
self._params.put_bool("AlphaLongitudinalEnabled", True)
|
||||||
|
self._params.put_bool("OnroadCycleRequested", True)
|
||||||
|
self._update_toggles()
|
||||||
|
else:
|
||||||
|
self._alpha_long_toggle.action_item.set_state(False)
|
||||||
|
|
||||||
|
# show confirmation dialog
|
||||||
|
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
|
||||||
|
f"<p>{self._alpha_long_toggle.description}</p>")
|
||||||
|
|
||||||
|
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
|
||||||
|
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._params.put_bool("AlphaLongitudinalEnabled", False)
|
||||||
|
self._params.put_bool("OnroadCycleRequested", True)
|
||||||
|
self._update_toggles()
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import math
|
||||||
|
|
||||||
|
from cereal import messaging, log
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
|
||||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||||
from openpilot.system.hardware import TICI
|
from openpilot.system.hardware import TICI
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
|
from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop
|
||||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
|
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
|
||||||
from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||||
from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
|
from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
|
||||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
|
||||||
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
|
||||||
'reset_calibration': (
|
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
|
||||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
|
||||||
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
|
||||||
),
|
|
||||||
'review_guide': "Review the rules, features, and limitations of openpilot",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -35,49 +36,61 @@ class DeviceLayout(Widget):
|
|||||||
self._select_language_dialog: MultiOptionDialog | None = None
|
self._select_language_dialog: MultiOptionDialog | None = None
|
||||||
self._driver_camera: DriverCameraDialog | None = None
|
self._driver_camera: DriverCameraDialog | None = None
|
||||||
self._pair_device_dialog: PairingDialog | None = None
|
self._pair_device_dialog: PairingDialog | None = None
|
||||||
self._fcc_dialog: HtmlRenderer | None = None
|
self._fcc_dialog: HtmlModal | None = None
|
||||||
|
self._training_guide: TrainingGuide | None = None
|
||||||
|
|
||||||
items = self._initialize_items()
|
items = self._initialize_items()
|
||||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||||
|
|
||||||
|
ui_state.add_offroad_transition_callback(self._offroad_transition)
|
||||||
|
|
||||||
def _initialize_items(self):
|
def _initialize_items(self):
|
||||||
dongle_id = self._params.get("DongleId") or "N/A"
|
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device)
|
||||||
serial = self._params.get("HardwareSerial") or "N/A"
|
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
|
||||||
|
|
||||||
|
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
|
||||||
|
callback=self._reset_calibration_prompt)
|
||||||
|
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
|
||||||
|
|
||||||
|
self._power_off_btn = dual_button_item(lambda: tr("Reboot"), lambda: tr("Power Off"),
|
||||||
|
left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
text_item("Dongle ID", dongle_id),
|
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
|
||||||
text_item("Serial", serial),
|
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
|
||||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
self._pair_device_btn,
|
||||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
|
||||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||||
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
|
self._reset_calib_btn,
|
||||||
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
|
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
|
||||||
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
self._on_review_training_guide, enabled=ui_state.is_offroad),
|
||||||
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
|
regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
|
||||||
|
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
|
||||||
|
self._power_off_btn,
|
||||||
]
|
]
|
||||||
regulatory_btn.set_visible(TICI)
|
regulatory_btn.set_visible(TICI)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def _offroad_transition(self):
|
||||||
|
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
self._scroller.show_event()
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
self._scroller.render(rect)
|
self._scroller.render(rect)
|
||||||
|
|
||||||
def _show_language_selection(self):
|
def _show_language_dialog(self):
|
||||||
try:
|
def handle_language_selection(result: int):
|
||||||
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
|
if result == 1 and self._select_language_dialog:
|
||||||
with open(languages_file, encoding='utf-8') as f:
|
selected_language = multilang.languages[self._select_language_dialog.selection]
|
||||||
languages = json.load(f)
|
multilang.change_language(selected_language)
|
||||||
|
self._update_calib_description()
|
||||||
|
self._select_language_dialog = None
|
||||||
|
|
||||||
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
|
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
|
||||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
|
option_font_weight=FontWeight.UNIFONT)
|
||||||
except FileNotFoundError:
|
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
|
||||||
pass
|
|
||||||
|
|
||||||
def _handle_language_selection(self, result: int):
|
|
||||||
if result == 1 and self._select_language_dialog:
|
|
||||||
selected_language = self._select_language_dialog.selection
|
|
||||||
self._params.put("LanguageSetting", selected_language)
|
|
||||||
|
|
||||||
self._select_language_dialog = None
|
|
||||||
|
|
||||||
def _show_driver_camera(self):
|
def _show_driver_camera(self):
|
||||||
if not self._driver_camera:
|
if not self._driver_camera:
|
||||||
@@ -87,34 +100,80 @@ class DeviceLayout(Widget):
|
|||||||
|
|
||||||
def _reset_calibration_prompt(self):
|
def _reset_calibration_prompt(self):
|
||||||
if ui_state.engaged:
|
if ui_state.engaged:
|
||||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration"))
|
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
|
||||||
return
|
return
|
||||||
|
|
||||||
gui_app.set_modal_overlay(
|
def reset_calibration(result: int):
|
||||||
lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"),
|
# Check engaged again in case it changed while the dialog was open
|
||||||
callback=self._reset_calibration,
|
if ui_state.engaged or result != DialogResult.CONFIRM:
|
||||||
)
|
return
|
||||||
|
|
||||||
def _reset_calibration(self, result: int):
|
self._params.remove("CalibrationParams")
|
||||||
if ui_state.engaged or result != DialogResult.CONFIRM:
|
self._params.remove("LiveTorqueParameters")
|
||||||
return
|
self._params.remove("LiveParameters")
|
||||||
|
self._params.remove("LiveParametersV2")
|
||||||
|
self._params.remove("LiveDelay")
|
||||||
|
self._params.put_bool("OnroadCycleRequested", True)
|
||||||
|
self._update_calib_description()
|
||||||
|
|
||||||
self._params.remove("CalibrationParams")
|
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
|
||||||
self._params.remove("LiveTorqueParameters")
|
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
|
||||||
self._params.remove("LiveParameters")
|
|
||||||
self._params.remove("LiveParametersV2")
|
def _update_calib_description(self):
|
||||||
self._params.remove("LiveDelay")
|
desc = tr(DESCRIPTIONS['reset_calibration'])
|
||||||
self._params.put_bool("OnroadCycleRequested", True)
|
|
||||||
|
calib_bytes = self._params.get("CalibrationParams")
|
||||||
|
if calib_bytes:
|
||||||
|
try:
|
||||||
|
calib = messaging.log_from_bytes(calib_bytes, log.Event).liveCalibration
|
||||||
|
|
||||||
|
if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated:
|
||||||
|
pitch = math.degrees(calib.rpyCalib[1])
|
||||||
|
yaw = math.degrees(calib.rpyCalib[2])
|
||||||
|
desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"),
|
||||||
|
abs(yaw), tr("left") if yaw > 0 else tr("right"))
|
||||||
|
except Exception:
|
||||||
|
cloudlog.exception("invalid CalibrationParams")
|
||||||
|
|
||||||
|
lag_perc = 0
|
||||||
|
lag_bytes = self._params.get("LiveDelay")
|
||||||
|
if lag_bytes:
|
||||||
|
try:
|
||||||
|
lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).liveDelay.calPerc
|
||||||
|
except Exception:
|
||||||
|
cloudlog.exception("invalid LiveDelay")
|
||||||
|
if lag_perc < 100:
|
||||||
|
desc += tr("<br><br>Steering lag calibration is {}% complete.").format(lag_perc)
|
||||||
|
else:
|
||||||
|
desc += tr("<br><br>Steering lag calibration is complete.")
|
||||||
|
|
||||||
|
torque_bytes = self._params.get("LiveTorqueParameters")
|
||||||
|
if torque_bytes:
|
||||||
|
try:
|
||||||
|
torque = messaging.log_from_bytes(torque_bytes, log.Event).liveTorqueParameters
|
||||||
|
# don't add for non-torque cars
|
||||||
|
if torque.useParams:
|
||||||
|
torque_perc = torque.calPerc
|
||||||
|
if torque_perc < 100:
|
||||||
|
desc += tr(" Steering torque response calibration is {}% complete.").format(torque_perc)
|
||||||
|
else:
|
||||||
|
desc += tr(" Steering torque response calibration is complete.")
|
||||||
|
except Exception:
|
||||||
|
cloudlog.exception("invalid LiveTorqueParameters")
|
||||||
|
|
||||||
|
desc += "<br><br>"
|
||||||
|
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
|
||||||
|
"Resetting calibration will restart openpilot if the car is powered on.")
|
||||||
|
|
||||||
|
self._reset_calib_btn.set_description(desc)
|
||||||
|
|
||||||
def _reboot_prompt(self):
|
def _reboot_prompt(self):
|
||||||
if ui_state.engaged:
|
if ui_state.engaged:
|
||||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot"))
|
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot")))
|
||||||
return
|
return
|
||||||
|
|
||||||
gui_app.set_modal_overlay(
|
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
|
||||||
lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"),
|
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
|
||||||
callback=self._perform_reboot,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _perform_reboot(self, result: int):
|
def _perform_reboot(self, result: int):
|
||||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||||
@@ -122,13 +181,11 @@ class DeviceLayout(Widget):
|
|||||||
|
|
||||||
def _power_off_prompt(self):
|
def _power_off_prompt(self):
|
||||||
if ui_state.engaged:
|
if ui_state.engaged:
|
||||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off"))
|
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
|
||||||
return
|
return
|
||||||
|
|
||||||
gui_app.set_modal_overlay(
|
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
|
||||||
lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"),
|
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
|
||||||
callback=self._perform_power_off,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _perform_power_off(self, result: int):
|
def _perform_power_off(self, result: int):
|
||||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||||
@@ -141,10 +198,13 @@ class DeviceLayout(Widget):
|
|||||||
|
|
||||||
def _on_regulatory(self):
|
def _on_regulatory(self):
|
||||||
if not self._fcc_dialog:
|
if not self._fcc_dialog:
|
||||||
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
||||||
|
gui_app.set_modal_overlay(self._fcc_dialog)
|
||||||
|
|
||||||
gui_app.set_modal_overlay(self._fcc_dialog,
|
def _on_review_training_guide(self):
|
||||||
callback=lambda result: setattr(self, '_fcc_dialog', None),
|
if not self._training_guide:
|
||||||
)
|
def completed_callback():
|
||||||
|
gui_app.set_modal_overlay(None)
|
||||||
|
|
||||||
def _on_review_training_guide(self): pass
|
self._training_guide = TrainingGuide(completed_callback=completed_callback)
|
||||||
|
gui_app.set_modal_overlay(self._training_guide)
|
||||||
|
|||||||
@@ -7,21 +7,23 @@ from openpilot.common.params import Params
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||||
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||||
|
|
||||||
TITLE = "Firehose Mode"
|
TITLE = tr_noop("Firehose Mode")
|
||||||
DESCRIPTION = (
|
DESCRIPTION = tr_noop(
|
||||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||||
)
|
)
|
||||||
INSTRUCTIONS = (
|
INSTRUCTIONS = tr_noop(
|
||||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
||||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
|
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
|
||||||
+ "Frequently Asked Questions\n\n"
|
+ "Frequently Asked Questions\n\n"
|
||||||
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
|
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
|
||||||
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
|
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
|
||||||
@@ -43,12 +45,16 @@ class FirehoseLayout(Widget):
|
|||||||
self.params = Params()
|
self.params = Params()
|
||||||
self.segment_count = self._get_segment_count()
|
self.segment_count = self._get_segment_count()
|
||||||
self.scroll_panel = GuiScrollPanel()
|
self.scroll_panel = GuiScrollPanel()
|
||||||
|
self._content_height = 0
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||||
self.update_thread.start()
|
self.update_thread.start()
|
||||||
self.last_update_time = 0
|
self.last_update_time = 0
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
self.scroll_panel.set_offset(0)
|
||||||
|
|
||||||
def _get_segment_count(self) -> int:
|
def _get_segment_count(self) -> int:
|
||||||
stats = self.params.get(self.PARAM_KEY)
|
stats = self.params.get(self.PARAM_KEY)
|
||||||
if not stats:
|
if not stats:
|
||||||
@@ -66,97 +72,72 @@ class FirehoseLayout(Widget):
|
|||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
# Calculate content dimensions
|
# Calculate content dimensions
|
||||||
content_width = rect.width - 80
|
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
|
||||||
content_height = self._calculate_content_height(int(content_width))
|
|
||||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
|
||||||
|
|
||||||
# Handle scrolling and render with clipping
|
# Handle scrolling and render with clipping
|
||||||
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
scroll_offset = self.scroll_panel.update(rect, content_rect)
|
||||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||||
self._render_content(rect, scroll_offset)
|
self._content_height = self._render_content(rect, scroll_offset)
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
def _calculate_content_height(self, content_width: int) -> int:
|
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
|
||||||
height = 80 # Top margin
|
|
||||||
|
|
||||||
# Title
|
|
||||||
height += 100 + 40
|
|
||||||
|
|
||||||
# Description
|
|
||||||
desc_font = gui_app.font(FontWeight.NORMAL)
|
|
||||||
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
|
|
||||||
height += len(desc_lines) * 45 + 40
|
|
||||||
|
|
||||||
# Status section
|
|
||||||
height += 32 # Separator
|
|
||||||
status_text, _ = self._get_status()
|
|
||||||
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
|
|
||||||
height += len(status_lines) * 60 + 20
|
|
||||||
|
|
||||||
# Contribution count (if available)
|
|
||||||
if self.segment_count > 0:
|
|
||||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
|
||||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
|
|
||||||
height += len(contrib_lines) * 52 + 20
|
|
||||||
|
|
||||||
# Instructions section
|
|
||||||
height += 32 # Separator
|
|
||||||
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
|
|
||||||
height += len(inst_lines) * 40 + 40 # Bottom margin
|
|
||||||
|
|
||||||
return height
|
|
||||||
|
|
||||||
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
|
||||||
x = int(rect.x + 40)
|
x = int(rect.x + 40)
|
||||||
y = int(rect.y + 40 + scroll_offset.y)
|
y = int(rect.y + 40 + scroll_offset)
|
||||||
w = int(rect.width - 80)
|
w = int(rect.width - 80)
|
||||||
|
|
||||||
# Title
|
# Title (centered)
|
||||||
|
title_text = tr(TITLE) # live translate
|
||||||
title_font = gui_app.font(FontWeight.MEDIUM)
|
title_font = gui_app.font(FontWeight.MEDIUM)
|
||||||
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
|
text_width = measure_text_cached(title_font, title_text, 100).x
|
||||||
y += 140
|
title_x = rect.x + (rect.width - text_width) / 2
|
||||||
|
rl.draw_text_ex(title_font, title_text, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
|
||||||
|
y += 200
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
y = self._draw_wrapped_text(x, y, w, tr(DESCRIPTION), gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||||
y += 40
|
y += 40 + 20
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||||
y += 30
|
y += 30 + 20
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
status_text, status_color = self._get_status()
|
status_text, status_color = self._get_status()
|
||||||
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
||||||
y += 20
|
y += 20 + 20
|
||||||
|
|
||||||
# Contribution count (if available)
|
# Contribution count (if available)
|
||||||
if self.segment_count > 0:
|
if self.segment_count > 0:
|
||||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
||||||
|
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
|
||||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||||
y += 20
|
y += 20 + 20
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||||
y += 30
|
y += 30 + 20
|
||||||
|
|
||||||
# Instructions
|
# Instructions
|
||||||
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||||
|
|
||||||
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
|
# bottom margin + remove effect of scroll offset
|
||||||
wrapped = wrap_text(font, text, size, width)
|
return int(round(y - self.scroll_panel.offset + 40))
|
||||||
|
|
||||||
|
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
|
||||||
|
wrapped = wrap_text(font, text, font_size, width)
|
||||||
for line in wrapped:
|
for line in wrapped:
|
||||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
|
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
|
||||||
y += size
|
y += font_size * FONT_SCALE
|
||||||
return y
|
return round(y)
|
||||||
|
|
||||||
def _get_status(self) -> tuple[str, rl.Color]:
|
def _get_status(self) -> tuple[str, rl.Color]:
|
||||||
network_type = ui_state.sm["deviceState"].networkType
|
network_type = ui_state.sm["deviceState"].networkType
|
||||||
network_metered = ui_state.sm["deviceState"].networkMetered
|
network_metered = ui_state.sm["deviceState"].networkMetered
|
||||||
|
|
||||||
if not network_metered and network_type != 0: # Not metered and connected
|
if not network_metered and network_type != 0: # Not metered and connected
|
||||||
return "ACTIVE", self.GREEN
|
return tr("ACTIVE"), self.GREEN
|
||||||
else:
|
else:
|
||||||
return "INACTIVE: connect to an unmetered network", self.RED
|
return tr("INACTIVE: connect to an unmetered network"), self.RED
|
||||||
|
|
||||||
def _fetch_firehose_stats(self):
|
def _fetch_firehose_stats(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -8,18 +8,16 @@ from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
|||||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
from openpilot.system.ui.widgets.network import NetworkUI
|
||||||
|
|
||||||
# Settings close button
|
|
||||||
SETTINGS_CLOSE_TEXT = "×"
|
|
||||||
SETTINGS_CLOSE_TEXT_Y_OFFSET = 8 # The '×' character isn't quite vertically centered in the font so we need to offset it a bit to fully center it
|
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
SIDEBAR_WIDTH = 500
|
SIDEBAR_WIDTH = 500
|
||||||
CLOSE_BTN_SIZE = 200
|
CLOSE_BTN_SIZE = 200
|
||||||
|
CLOSE_ICON_SIZE = 70
|
||||||
NAV_BTN_HEIGHT = 110
|
NAV_BTN_HEIGHT = 110
|
||||||
PANEL_MARGIN = 50
|
PANEL_MARGIN = 50
|
||||||
|
|
||||||
@@ -58,15 +56,16 @@ class SettingsLayout(Widget):
|
|||||||
wifi_manager.set_active(False)
|
wifi_manager.set_active(False)
|
||||||
|
|
||||||
self._panels = {
|
self._panels = {
|
||||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
|
PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()),
|
||||||
PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)),
|
PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)),
|
||||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
|
PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
|
||||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
|
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
|
||||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
|
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
|
||||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
|
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
|
||||||
}
|
}
|
||||||
|
|
||||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||||
|
self._close_icon = gui_app.texture("icons/close2.png", CLOSE_ICON_SIZE, CLOSE_ICON_SIZE)
|
||||||
|
|
||||||
# Callbacks
|
# Callbacks
|
||||||
self._close_callback: Callable | None = None
|
self._close_callback: Callable | None = None
|
||||||
@@ -96,12 +95,21 @@ class SettingsLayout(Widget):
|
|||||||
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
|
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
|
||||||
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
||||||
|
|
||||||
close_text_size = measure_text_cached(self._font_medium, SETTINGS_CLOSE_TEXT, 140)
|
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
|
||||||
close_text_pos = rl.Vector2(
|
icon_dest = rl.Rectangle(
|
||||||
close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2,
|
close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2,
|
||||||
close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2 - SETTINGS_CLOSE_TEXT_Y_OFFSET,
|
close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2,
|
||||||
|
self._close_icon.width,
|
||||||
|
self._close_icon.height,
|
||||||
|
)
|
||||||
|
rl.draw_texture_pro(
|
||||||
|
self._close_icon,
|
||||||
|
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
|
||||||
|
icon_dest,
|
||||||
|
rl.Vector2(0, 0),
|
||||||
|
0,
|
||||||
|
icon_color,
|
||||||
)
|
)
|
||||||
rl.draw_text_ex(self._font_medium, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED)
|
|
||||||
|
|
||||||
# Store close button rect for click detection
|
# Store close button rect for click detection
|
||||||
self._close_btn_rect = close_btn_rect
|
self._close_btn_rect = close_btn_rect
|
||||||
@@ -115,11 +123,12 @@ class SettingsLayout(Widget):
|
|||||||
is_selected = panel_type == self._current_panel
|
is_selected = panel_type == self._current_panel
|
||||||
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
|
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
|
||||||
# Draw button text (right-aligned)
|
# Draw button text (right-aligned)
|
||||||
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
|
panel_name = tr(panel_info.name)
|
||||||
|
text_size = measure_text_cached(self._font_medium, panel_name, 65)
|
||||||
text_pos = rl.Vector2(
|
text_pos = rl.Vector2(
|
||||||
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
||||||
)
|
)
|
||||||
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
|
rl.draw_text_ex(self._font_medium, panel_name, text_pos, 65, 0, text_color)
|
||||||
|
|
||||||
# Store button rect for click detection
|
# Store button rect for click detection
|
||||||
panel_info.button_rect = button_rect
|
panel_info.button_rect = button_rect
|
||||||
|
|||||||
@@ -1,42 +1,194 @@
|
|||||||
from openpilot.common.params import Params
|
import os
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
from openpilot.common.time_helpers import system_time_valid
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, trn
|
||||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
|
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||||
from openpilot.system.ui.widgets.list_view import button_item, text_item
|
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
|
||||||
|
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
|
|
||||||
|
# TODO: remove this. updater fails to respond on startup if time is not correct
|
||||||
|
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
|
||||||
|
|
||||||
|
|
||||||
|
def time_ago(date: datetime.datetime | None) -> str:
|
||||||
|
if not date:
|
||||||
|
return tr("never")
|
||||||
|
|
||||||
|
if not system_time_valid():
|
||||||
|
return date.strftime("%a %b %d %Y")
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.UTC)
|
||||||
|
if date.tzinfo is None:
|
||||||
|
date = date.replace(tzinfo=datetime.UTC)
|
||||||
|
|
||||||
|
diff_seconds = int((now - date).total_seconds())
|
||||||
|
if diff_seconds < 60:
|
||||||
|
return tr("now")
|
||||||
|
if diff_seconds < 3600:
|
||||||
|
m = diff_seconds // 60
|
||||||
|
return trn("{} minute ago", "{} minutes ago", m).format(m)
|
||||||
|
if diff_seconds < 86400:
|
||||||
|
h = diff_seconds // 3600
|
||||||
|
return trn("{} hour ago", "{} hours ago", h).format(h)
|
||||||
|
if diff_seconds < 604800:
|
||||||
|
d = diff_seconds // 86400
|
||||||
|
return trn("{} day ago", "{} days ago", d).format(d)
|
||||||
|
return date.strftime("%a %b %d %Y")
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLayout(Widget):
|
class SoftwareLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._params = Params()
|
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
|
||||||
items = self._init_items()
|
self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
|
||||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update)
|
||||||
|
|
||||||
def _init_items(self):
|
# Install button is initially hidden
|
||||||
items = [
|
self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update)
|
||||||
text_item("Current Version", ""),
|
self._install_btn.set_visible(False)
|
||||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
|
||||||
button_item("Install Update", "INSTALL", callback=self._on_install_update),
|
# Track waiting-for-updater transition to avoid brief re-enable while still idle
|
||||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
self._waiting_for_updater = False
|
||||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
self._waiting_start_ts: float = 0.0
|
||||||
]
|
|
||||||
return items
|
# Branch switcher
|
||||||
|
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
|
||||||
|
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||||
|
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
|
||||||
|
self._branch_dialog: MultiOptionDialog | None = None
|
||||||
|
|
||||||
|
self._scroller = Scroller([
|
||||||
|
self._onroad_label,
|
||||||
|
self._version_item,
|
||||||
|
self._download_btn,
|
||||||
|
self._install_btn,
|
||||||
|
self._branch_btn,
|
||||||
|
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
|
||||||
|
], line_separator=True, spacing=0)
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
self._scroller.show_event()
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
self._scroller.render(rect)
|
self._scroller.render(rect)
|
||||||
|
|
||||||
def _on_download_update(self): pass
|
def _update_state(self):
|
||||||
def _on_install_update(self): pass
|
# Show/hide onroad warning
|
||||||
def _on_select_branch(self): pass
|
self._onroad_label.set_visible(ui_state.is_onroad())
|
||||||
|
|
||||||
|
# Update current version and release notes
|
||||||
|
current_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
|
||||||
|
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
|
||||||
|
self._version_item.action_item.set_text(current_desc)
|
||||||
|
self._version_item.set_description(current_release_notes)
|
||||||
|
|
||||||
|
# Update download button visibility and state
|
||||||
|
self._download_btn.set_visible(ui_state.is_offroad())
|
||||||
|
|
||||||
|
updater_state = ui_state.params.get("UpdaterState") or "idle"
|
||||||
|
failed_count = ui_state.params.get("UpdateFailedCount") or 0
|
||||||
|
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
|
||||||
|
update_available = ui_state.params.get_bool("UpdateAvailable")
|
||||||
|
|
||||||
|
if updater_state != "idle":
|
||||||
|
# Updater responded
|
||||||
|
self._waiting_for_updater = False
|
||||||
|
self._download_btn.action_item.set_enabled(False)
|
||||||
|
self._download_btn.action_item.set_value(updater_state)
|
||||||
|
else:
|
||||||
|
if failed_count > 0:
|
||||||
|
self._download_btn.action_item.set_value(tr("failed to check for update"))
|
||||||
|
self._download_btn.action_item.set_text(tr("CHECK"))
|
||||||
|
elif fetch_available:
|
||||||
|
self._download_btn.action_item.set_value(tr("update available"))
|
||||||
|
self._download_btn.action_item.set_text(tr("DOWNLOAD"))
|
||||||
|
else:
|
||||||
|
last_update = ui_state.params.get("LastUpdateTime")
|
||||||
|
if last_update:
|
||||||
|
formatted = time_ago(last_update)
|
||||||
|
self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted))
|
||||||
|
else:
|
||||||
|
self._download_btn.action_item.set_value(tr("up to date, last checked never"))
|
||||||
|
self._download_btn.action_item.set_text(tr("CHECK"))
|
||||||
|
|
||||||
|
# If we've been waiting too long without a state change, reset state
|
||||||
|
if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT):
|
||||||
|
self._waiting_for_updater = False
|
||||||
|
|
||||||
|
# Only enable if we're not waiting for updater to flip out of idle
|
||||||
|
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
|
||||||
|
|
||||||
|
# Update target branch button value
|
||||||
|
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||||
|
self._branch_btn.action_item.set_value(current_branch)
|
||||||
|
|
||||||
|
# Update install button
|
||||||
|
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
|
||||||
|
if update_available:
|
||||||
|
new_desc = ui_state.params.get("UpdaterNewDescription") or ""
|
||||||
|
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
|
||||||
|
self._install_btn.action_item.set_text(tr("INSTALL"))
|
||||||
|
self._install_btn.action_item.set_value(new_desc)
|
||||||
|
self._install_btn.set_description(new_release_notes)
|
||||||
|
# Enable install button for testing (like Qt showEvent)
|
||||||
|
self._install_btn.action_item.set_enabled(True)
|
||||||
|
else:
|
||||||
|
self._install_btn.set_visible(False)
|
||||||
|
|
||||||
|
def _on_download_update(self):
|
||||||
|
# Check if we should start checking or start downloading
|
||||||
|
self._download_btn.action_item.set_enabled(False)
|
||||||
|
if self._download_btn.action_item.text == tr("CHECK"):
|
||||||
|
# Start checking for updates
|
||||||
|
self._waiting_for_updater = True
|
||||||
|
self._waiting_start_ts = time.monotonic()
|
||||||
|
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||||
|
else:
|
||||||
|
# Start downloading
|
||||||
|
self._waiting_for_updater = True
|
||||||
|
self._waiting_start_ts = time.monotonic()
|
||||||
|
os.system("pkill -SIGHUP -f system.updated.updated")
|
||||||
|
|
||||||
def _on_uninstall(self):
|
def _on_uninstall(self):
|
||||||
def handle_uninstall_confirmation(result):
|
def handle_uninstall_confirmation(result):
|
||||||
if result == DialogResult.CONFIRM:
|
if result == DialogResult.CONFIRM:
|
||||||
self._params.put_bool("DoUninstall", True)
|
ui_state.params.put_bool("DoUninstall", True)
|
||||||
|
|
||||||
gui_app.set_modal_overlay(
|
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
|
||||||
lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"),
|
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
|
||||||
callback=handle_uninstall_confirmation,
|
|
||||||
)
|
def _on_install_update(self):
|
||||||
|
# Trigger reboot to install update
|
||||||
|
self._install_btn.action_item.set_enabled(False)
|
||||||
|
ui_state.params.put_bool("DoReboot", True)
|
||||||
|
|
||||||
|
def _on_select_branch(self):
|
||||||
|
# Get available branches and order
|
||||||
|
current_git_branch = ui_state.params.get("GitBranch") or ""
|
||||||
|
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
|
||||||
|
branches = [b for b in branches_str.split(",") if b]
|
||||||
|
|
||||||
|
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
|
||||||
|
if b in branches:
|
||||||
|
branches.remove(b)
|
||||||
|
branches.insert(0, b)
|
||||||
|
|
||||||
|
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||||
|
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target)
|
||||||
|
|
||||||
|
def handle_selection(result):
|
||||||
|
# Confirmed selection
|
||||||
|
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
|
||||||
|
selection = self._branch_dialog.selection
|
||||||
|
ui_state.params.put("UpdaterTargetBranch", selection)
|
||||||
|
self._branch_btn.action_item.set_value(selection)
|
||||||
|
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||||
|
self._branch_dialog = None
|
||||||
|
|
||||||
|
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)
|
||||||
|
|||||||
@@ -1,28 +1,40 @@
|
|||||||
from openpilot.common.params import Params
|
from cereal import log
|
||||||
|
from openpilot.common.params import Params, UnknownKeyName
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
|
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||||
|
from openpilot.system.ui.widgets import DialogResult
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
if Params().get_bool("sunnypilot_ui"):
|
||||||
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item
|
||||||
|
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp as multiple_button_item
|
||||||
|
|
||||||
|
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
"OpenpilotEnabledToggle": (
|
"OpenpilotEnabledToggle": tr_noop(
|
||||||
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||||
"Your attention is required at all times to use this feature."
|
"Your attention is required at all times to use this feature."
|
||||||
),
|
),
|
||||||
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||||
"LongitudinalPersonality": (
|
"LongitudinalPersonality": tr_noop(
|
||||||
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||||
"your steering wheel distance button."
|
"your steering wheel distance button."
|
||||||
),
|
),
|
||||||
"IsLdwEnabled": (
|
"IsLdwEnabled": tr_noop(
|
||||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||||
),
|
),
|
||||||
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
|
||||||
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||||
"IsMetric": "Display speed in km/h instead of mph.",
|
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||||
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.",
|
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -30,66 +42,207 @@ class TogglesLayout(Widget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
items = [
|
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||||
toggle_item(
|
|
||||||
"Enable openpilot",
|
|
||||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
|
||||||
self._params.get_bool("OpenpilotEnabledToggle"),
|
|
||||||
icon="chffr_wheel.png",
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Experimental Mode",
|
|
||||||
initial_state=self._params.get_bool("ExperimentalMode"),
|
|
||||||
icon="experimental_white.png",
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Disengage on Accelerator Pedal",
|
|
||||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
|
||||||
self._params.get_bool("DisengageOnAccelerator"),
|
|
||||||
icon="disengage_on_accelerator.png",
|
|
||||||
),
|
|
||||||
multiple_button_item(
|
|
||||||
"Driving Personality",
|
|
||||||
DESCRIPTIONS["LongitudinalPersonality"],
|
|
||||||
buttons=["Aggressive", "Standard", "Relaxed"],
|
|
||||||
button_width=255,
|
|
||||||
callback=self._set_longitudinal_personality,
|
|
||||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
|
||||||
icon="speed_limit.png"
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Enable Lane Departure Warnings",
|
|
||||||
DESCRIPTIONS["IsLdwEnabled"],
|
|
||||||
self._params.get_bool("IsLdwEnabled"),
|
|
||||||
icon="warning.png",
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Always-On Driver Monitoring",
|
|
||||||
DESCRIPTIONS["AlwaysOnDM"],
|
|
||||||
self._params.get_bool("AlwaysOnDM"),
|
|
||||||
icon="monitoring.png",
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Record and Upload Driver Camera",
|
|
||||||
DESCRIPTIONS["RecordFront"],
|
|
||||||
self._params.get_bool("RecordFront"),
|
|
||||||
icon="monitoring.png",
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Record Microphone Audio",
|
|
||||||
DESCRIPTIONS["RecordAudio"],
|
|
||||||
self._params.get_bool("RecordAudio"),
|
|
||||||
icon="microphone.png",
|
|
||||||
),
|
|
||||||
toggle_item(
|
|
||||||
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
# param, title, desc, icon, needs_restart
|
||||||
|
self._toggle_defs = {
|
||||||
|
"OpenpilotEnabledToggle": (
|
||||||
|
lambda: tr("Enable openpilot"),
|
||||||
|
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||||
|
"chffr_wheel.png",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
"ExperimentalMode": (
|
||||||
|
lambda: tr("Experimental Mode"),
|
||||||
|
"",
|
||||||
|
"experimental_white.png",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"DisengageOnAccelerator": (
|
||||||
|
lambda: tr("Disengage on Accelerator Pedal"),
|
||||||
|
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||||
|
"disengage_on_accelerator.png",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"IsLdwEnabled": (
|
||||||
|
lambda: tr("Enable Lane Departure Warnings"),
|
||||||
|
DESCRIPTIONS["IsLdwEnabled"],
|
||||||
|
"warning.png",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"AlwaysOnDM": (
|
||||||
|
lambda: tr("Always-On Driver Monitoring"),
|
||||||
|
DESCRIPTIONS["AlwaysOnDM"],
|
||||||
|
"monitoring.png",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"RecordFront": (
|
||||||
|
lambda: tr("Record and Upload Driver Camera"),
|
||||||
|
DESCRIPTIONS["RecordFront"],
|
||||||
|
"monitoring.png",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
"RecordAudio": (
|
||||||
|
lambda: tr("Record and Upload Microphone Audio"),
|
||||||
|
DESCRIPTIONS["RecordAudio"],
|
||||||
|
"microphone.png",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
"IsMetric": (
|
||||||
|
lambda: tr("Use Metric System"),
|
||||||
|
DESCRIPTIONS["IsMetric"],
|
||||||
|
"metric.png",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
self._long_personality_setting = multiple_button_item(
|
||||||
|
lambda: tr("Driving Personality"),
|
||||||
|
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||||
|
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||||
|
button_width=255,
|
||||||
|
callback=self._set_longitudinal_personality,
|
||||||
|
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||||
|
icon="speed_limit.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._toggles = {}
|
||||||
|
self._locked_toggles = set()
|
||||||
|
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
||||||
|
toggle = toggle_item(
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
self._params.get_bool(param),
|
||||||
|
callback=lambda state, p=param: self._toggle_callback(state, p),
|
||||||
|
icon=icon,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
locked = self._params.get_bool(param + "Lock")
|
||||||
|
except UnknownKeyName:
|
||||||
|
locked = False
|
||||||
|
toggle.action_item.set_enabled(not locked)
|
||||||
|
|
||||||
|
# Make description callable for live translation
|
||||||
|
additional_desc = ""
|
||||||
|
if needs_restart and not locked:
|
||||||
|
additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.")
|
||||||
|
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
|
||||||
|
|
||||||
|
# track for engaged state updates
|
||||||
|
if locked:
|
||||||
|
self._locked_toggles.add(param)
|
||||||
|
|
||||||
|
self._toggles[param] = toggle
|
||||||
|
|
||||||
|
# insert longitudinal personality after NDOG toggle
|
||||||
|
if param == "DisengageOnAccelerator":
|
||||||
|
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
|
||||||
|
|
||||||
|
self._update_experimental_mode_icon()
|
||||||
|
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||||
|
|
||||||
|
ui_state.add_engaged_transition_callback(self._update_toggles)
|
||||||
|
|
||||||
|
def _update_state(self):
|
||||||
|
if ui_state.sm.updated["selfdriveState"]:
|
||||||
|
personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality]
|
||||||
|
if personality != ui_state.personality and ui_state.started:
|
||||||
|
self._long_personality_setting.action_item.set_selected_button(personality)
|
||||||
|
ui_state.personality = personality
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
self._scroller.show_event()
|
||||||
|
self._update_toggles()
|
||||||
|
|
||||||
|
def _update_toggles(self):
|
||||||
|
ui_state.update_params()
|
||||||
|
|
||||||
|
e2e_description = tr(
|
||||||
|
"openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||||
|
"Experimental features are listed below:<br>" +
|
||||||
|
"<h4>End-to-End Longitudinal Control</h4><br>" +
|
||||||
|
"Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
|
||||||
|
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " +
|
||||||
|
"mistakes should be expected.<br>" +
|
||||||
|
"<h4>New Driving Visualization</h4><br>" +
|
||||||
|
"The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " +
|
||||||
|
"The Experimental mode logo will also be shown in the top right corner."
|
||||||
|
)
|
||||||
|
|
||||||
|
if ui_state.CP is not None:
|
||||||
|
if ui_state.has_longitudinal_control:
|
||||||
|
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
|
||||||
|
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||||
|
self._long_personality_setting.action_item.set_enabled(True)
|
||||||
|
else:
|
||||||
|
# no long for now
|
||||||
|
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
|
||||||
|
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||||
|
self._long_personality_setting.action_item.set_enabled(False)
|
||||||
|
self._params.remove("ExperimentalMode")
|
||||||
|
|
||||||
|
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
|
||||||
|
|
||||||
|
long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
|
||||||
|
if ui_state.CP.alphaLongitudinalAvailable:
|
||||||
|
if self._is_release:
|
||||||
|
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
|
||||||
|
"Experimental mode, on non-release branches.")
|
||||||
|
else:
|
||||||
|
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||||
|
|
||||||
|
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
|
||||||
|
else:
|
||||||
|
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||||
|
|
||||||
|
self._update_experimental_mode_icon()
|
||||||
|
|
||||||
|
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||||
|
# refresh toggles from params to mirror external changes
|
||||||
|
for param in self._toggle_defs:
|
||||||
|
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||||
|
|
||||||
|
# these toggles need restart, block while engaged
|
||||||
|
for toggle_def in self._toggle_defs:
|
||||||
|
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
|
||||||
|
self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged)
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
self._scroller.render(rect)
|
self._scroller.render(rect)
|
||||||
|
|
||||||
|
def _update_experimental_mode_icon(self):
|
||||||
|
icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
|
||||||
|
self._toggles["ExperimentalMode"].set_icon(icon)
|
||||||
|
|
||||||
|
def _handle_experimental_mode_toggle(self, state: bool):
|
||||||
|
confirmed = self._params.get_bool("ExperimentalModeConfirmed")
|
||||||
|
if state and not confirmed:
|
||||||
|
def confirm_callback(result: int):
|
||||||
|
if result == DialogResult.CONFIRM:
|
||||||
|
self._params.put_bool("ExperimentalMode", True)
|
||||||
|
self._params.put_bool("ExperimentalModeConfirmed", True)
|
||||||
|
else:
|
||||||
|
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||||
|
self._update_experimental_mode_icon()
|
||||||
|
|
||||||
|
# show confirmation dialog
|
||||||
|
content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
|
||||||
|
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
|
||||||
|
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
|
||||||
|
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
|
||||||
|
else:
|
||||||
|
self._update_experimental_mode_icon()
|
||||||
|
self._params.put_bool("ExperimentalMode", state)
|
||||||
|
|
||||||
|
def _toggle_callback(self, state: bool, param: str):
|
||||||
|
if param == "ExperimentalMode":
|
||||||
|
self._handle_experimental_mode_toggle(state)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._params.put_bool(param, state)
|
||||||
|
if self._toggle_defs[param][3]:
|
||||||
|
self._params.put_bool("OnroadCycleRequested", True)
|
||||||
|
|
||||||
def _set_longitudinal_personality(self, button_index: int):
|
def _set_longitudinal_personality(self, button_index: int):
|
||||||
self._params.put("LongitudinalPersonality", button_index)
|
self._params.put("LongitudinalPersonality", button_index)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from dataclasses import dataclass
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ NetworkType = log.DeviceState.NetworkType
|
|||||||
|
|
||||||
# Color scheme
|
# Color scheme
|
||||||
class Colors:
|
class Colors:
|
||||||
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
|
||||||
WHITE = rl.WHITE
|
WHITE = rl.WHITE
|
||||||
WHITE_DIM = rl.Color(255, 255, 255, 85)
|
WHITE_DIM = rl.Color(255, 255, 255, 85)
|
||||||
GRAY = rl.Color(84, 84, 84, 255)
|
GRAY = rl.Color(84, 84, 84, 255)
|
||||||
@@ -40,13 +40,13 @@ class Colors:
|
|||||||
|
|
||||||
|
|
||||||
NETWORK_TYPES = {
|
NETWORK_TYPES = {
|
||||||
NetworkType.none: "Offline",
|
NetworkType.none: tr_noop("--"),
|
||||||
NetworkType.wifi: "WiFi",
|
NetworkType.wifi: tr_noop("Wi-Fi"),
|
||||||
NetworkType.cell2G: "2G",
|
NetworkType.ethernet: tr_noop("ETH"),
|
||||||
NetworkType.cell3G: "3G",
|
NetworkType.cell2G: tr_noop("2G"),
|
||||||
NetworkType.cell4G: "LTE",
|
NetworkType.cell3G: tr_noop("3G"),
|
||||||
NetworkType.cell5G: "5G",
|
NetworkType.cell4G: tr_noop("LTE"),
|
||||||
NetworkType.ethernet: "Ethernet",
|
NetworkType.cell5G: tr_noop("5G"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -68,27 +68,33 @@ class Sidebar(Widget):
|
|||||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||||
self._net_strength = 0
|
self._net_strength = 0
|
||||||
|
|
||||||
self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD)
|
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||||
self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD)
|
self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||||
self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING)
|
self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||||
|
self._recording_audio = False
|
||||||
|
|
||||||
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
|
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
|
||||||
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
|
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
|
||||||
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
|
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
|
||||||
|
self._mic_img = gui_app.texture("icons/microphone.png", 30, 30)
|
||||||
|
self._mic_indicator_rect = rl.Rectangle(0, 0, 0, 0)
|
||||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
|
|
||||||
# Callbacks
|
# Callbacks
|
||||||
self._on_settings_click: Callable | None = None
|
self._on_settings_click: Callable | None = None
|
||||||
self._on_flag_click: Callable | None = None
|
self._on_flag_click: Callable | None = None
|
||||||
|
self._open_settings_callback: Callable | None = None
|
||||||
|
|
||||||
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None):
|
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None,
|
||||||
|
open_settings: Callable | None = None):
|
||||||
self._on_settings_click = on_settings
|
self._on_settings_click = on_settings
|
||||||
self._on_flag_click = on_flag
|
self._on_flag_click = on_flag
|
||||||
|
self._open_settings_callback = open_settings
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
# Background
|
# Background
|
||||||
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
rl.draw_rectangle_rec(rect, rl.BLACK)
|
||||||
|
|
||||||
self._draw_buttons(rect)
|
self._draw_buttons(rect)
|
||||||
self._draw_network_indicator(rect)
|
self._draw_network_indicator(rect)
|
||||||
@@ -101,13 +107,14 @@ class Sidebar(Widget):
|
|||||||
|
|
||||||
device_state = sm['deviceState']
|
device_state = sm['deviceState']
|
||||||
|
|
||||||
|
self._recording_audio = ui_state.recording_audio
|
||||||
self._update_network_status(device_state)
|
self._update_network_status(device_state)
|
||||||
self._update_temperature_status(device_state)
|
self._update_temperature_status(device_state)
|
||||||
self._update_connection_status(device_state)
|
self._update_connection_status(device_state)
|
||||||
self._update_panda_status()
|
self._update_panda_status()
|
||||||
|
|
||||||
def _update_network_status(self, device_state):
|
def _update_network_status(self, device_state):
|
||||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown")
|
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
|
||||||
strength = device_state.networkStrength
|
strength = device_state.networkStrength
|
||||||
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
|
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
|
||||||
|
|
||||||
@@ -115,26 +122,26 @@ class Sidebar(Widget):
|
|||||||
thermal_status = device_state.thermalStatus
|
thermal_status = device_state.thermalStatus
|
||||||
|
|
||||||
if thermal_status == ThermalStatus.green:
|
if thermal_status == ThermalStatus.green:
|
||||||
self._temp_status.update("TEMP", "GOOD", Colors.GOOD)
|
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||||
elif thermal_status == ThermalStatus.yellow:
|
elif thermal_status == ThermalStatus.yellow:
|
||||||
self._temp_status.update("TEMP", "OK", Colors.WARNING)
|
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING)
|
||||||
else:
|
else:
|
||||||
self._temp_status.update("TEMP", "HIGH", Colors.DANGER)
|
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)
|
||||||
|
|
||||||
def _update_connection_status(self, device_state):
|
def _update_connection_status(self, device_state):
|
||||||
last_ping = device_state.lastAthenaPingTime
|
last_ping = device_state.lastAthenaPingTime
|
||||||
if last_ping == 0:
|
if last_ping == 0:
|
||||||
self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING)
|
self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||||
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
|
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
|
||||||
self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD)
|
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD)
|
||||||
else:
|
else:
|
||||||
self._connect_status.update("CONNECT", "ERROR", Colors.DANGER)
|
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ERROR"), Colors.DANGER)
|
||||||
|
|
||||||
def _update_panda_status(self):
|
def _update_panda_status(self):
|
||||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||||
self._panda_status.update("NO", "PANDA", Colors.DANGER)
|
self._panda_status.update(tr_noop("NO"), tr_noop("PANDA"), Colors.DANGER)
|
||||||
else:
|
else:
|
||||||
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||||
@@ -143,6 +150,9 @@ class Sidebar(Widget):
|
|||||||
elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started:
|
elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started:
|
||||||
if self._on_flag_click:
|
if self._on_flag_click:
|
||||||
self._on_flag_click()
|
self._on_flag_click()
|
||||||
|
elif self._recording_audio and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect):
|
||||||
|
if self._open_settings_callback:
|
||||||
|
self._open_settings_callback()
|
||||||
|
|
||||||
def _draw_buttons(self, rect: rl.Rectangle):
|
def _draw_buttons(self, rect: rl.Rectangle):
|
||||||
mouse_pos = rl.get_mouse_position()
|
mouse_pos = rl.get_mouse_position()
|
||||||
@@ -160,6 +170,17 @@ class Sidebar(Widget):
|
|||||||
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
|
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
|
||||||
rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
|
rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
|
||||||
|
|
||||||
|
# Microphone button
|
||||||
|
if self._recording_audio:
|
||||||
|
self._mic_indicator_rect = rl.Rectangle(rect.x + rect.width - 130, rect.y + 245, 75, 40)
|
||||||
|
|
||||||
|
mic_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect)
|
||||||
|
bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER
|
||||||
|
|
||||||
|
rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color)
|
||||||
|
rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
|
||||||
|
int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE)
|
||||||
|
|
||||||
def _draw_network_indicator(self, rect: rl.Rectangle):
|
def _draw_network_indicator(self, rect: rl.Rectangle):
|
||||||
# Signal strength dots
|
# Signal strength dots
|
||||||
x_start = rect.x + 58
|
x_start = rect.x + 58
|
||||||
@@ -176,7 +197,7 @@ class Sidebar(Widget):
|
|||||||
# Network type text
|
# Network type text
|
||||||
text_y = rect.y + 247
|
text_y = rect.y + 247
|
||||||
text_pos = rl.Vector2(rect.x + 58, text_y)
|
text_pos = rl.Vector2(rect.x + 58, text_y)
|
||||||
rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE)
|
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||||
|
|
||||||
def _draw_metrics(self, rect: rl.Rectangle):
|
def _draw_metrics(self, rect: rl.Rectangle):
|
||||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||||
@@ -189,15 +210,15 @@ class Sidebar(Widget):
|
|||||||
# Draw colored left edge (clipped rounded rectangle)
|
# Draw colored left edge (clipped rounded rectangle)
|
||||||
edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118)
|
edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118)
|
||||||
rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height))
|
rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height))
|
||||||
rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color)
|
rl.draw_rectangle_rounded(edge_rect, 0.3, 10, metric.color)
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
# Draw border
|
# Draw border
|
||||||
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER)
|
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER)
|
||||||
|
|
||||||
# Draw label and value
|
# Draw label and value
|
||||||
labels = [metric.label, metric.value]
|
labels = [tr(metric.label), tr(metric.value)]
|
||||||
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE)
|
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
|
||||||
for text in labels:
|
for text in labels:
|
||||||
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
|
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
|
||||||
text_y += text_size.y
|
text_y += text_size.y
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import time
|
import time
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from openpilot.common.api import Api
|
from openpilot.common.api import Api
|
||||||
|
from openpilot.common.time_helpers import system_time_valid
|
||||||
|
|
||||||
TOKEN_EXPIRY_HOURS = 2
|
TOKEN_EXPIRY_HOURS = 2
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _get_token(dongle_id: str, t: int):
|
def _get_token(dongle_id: str, t: int):
|
||||||
|
if not system_time_valid():
|
||||||
|
raise RuntimeError("System time is not valid, cannot generate token")
|
||||||
|
|
||||||
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
|
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
|||||||
|
|
||||||
|
|
||||||
class PrimeType(IntEnum):
|
class PrimeType(IntEnum):
|
||||||
UNKNOWN = -2,
|
UNKNOWN = -2
|
||||||
UNPAIRED = -1,
|
UNPAIRED = -1
|
||||||
NONE = 0,
|
NONE = 0
|
||||||
MAGENTA = 1,
|
MAGENTA = 1
|
||||||
LITE = 2,
|
LITE = 2
|
||||||
BLUE = 3,
|
BLUE = 3
|
||||||
MAGENTA_NEW = 4,
|
MAGENTA_NEW = 4
|
||||||
PURPLE = 5,
|
PURPLE = 5
|
||||||
|
|
||||||
|
|
||||||
class PrimeState:
|
class PrimeState:
|
||||||
@@ -33,7 +33,6 @@ class PrimeState:
|
|||||||
|
|
||||||
self._running = False
|
self._running = False
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self.start()
|
|
||||||
|
|
||||||
def _load_initial_state(self) -> PrimeType:
|
def _load_initial_state(self) -> PrimeType:
|
||||||
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType")
|
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType")
|
||||||
@@ -96,5 +95,9 @@ class PrimeState:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return bool(self.prime_type > PrimeType.NONE)
|
return bool(self.prime_type > PrimeType.NONE)
|
||||||
|
|
||||||
|
def is_paired(self) -> bool:
|
||||||
|
with self._lock:
|
||||||
|
return self.prime_type > PrimeType.UNPAIRED
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
#include <sys/resource.h>
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QTranslator>
|
|
||||||
|
|
||||||
#include "system/hardware/hw.h"
|
|
||||||
#include "selfdrive/ui/qt/util.h"
|
|
||||||
#include "selfdrive/ui/qt/window.h"
|
|
||||||
|
|
||||||
#ifdef SUNNYPILOT
|
|
||||||
#include "selfdrive/ui/sunnypilot/qt/window.h"
|
|
||||||
#define MainWindow MainWindowSP
|
|
||||||
#else
|
|
||||||
#include "selfdrive/ui/qt/qt_window.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
setpriority(PRIO_PROCESS, 0, -20);
|
|
||||||
|
|
||||||
qInstallMessageHandler(swagLogMessageHandler);
|
|
||||||
initApp(argc, argv);
|
|
||||||
|
|
||||||
QTranslator translator;
|
|
||||||
QString translation_file = QString::fromStdString(Params().get("LanguageSetting"));
|
|
||||||
if (!translator.load(QString(":/%1").arg(translation_file)) && translation_file.length()) {
|
|
||||||
qCritical() << "Failed to load translation file:" << translation_file;
|
|
||||||
}
|
|
||||||
|
|
||||||
QApplication a(argc, argv);
|
|
||||||
a.installTranslator(&translator);
|
|
||||||
|
|
||||||
MainWindow w;
|
|
||||||
setMainWindow(&w);
|
|
||||||
a.installEventFilter(&w);
|
|
||||||
return a.exec();
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,11 @@ from dataclasses import dataclass
|
|||||||
from cereal import messaging, log
|
from cereal import messaging, log
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.hardware import TICI
|
from openpilot.system.hardware import TICI
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import gui_text_box
|
from openpilot.system.ui.widgets.label import Label
|
||||||
|
|
||||||
AlertSize = log.SelfdriveState.AlertSize
|
AlertSize = log.SelfdriveState.AlertSize
|
||||||
AlertStatus = log.SelfdriveState.AlertStatus
|
AlertStatus = log.SelfdriveState.AlertStatus
|
||||||
@@ -21,14 +22,19 @@ ALERT_FONT_SMALL = 66
|
|||||||
ALERT_FONT_MEDIUM = 74
|
ALERT_FONT_MEDIUM = 74
|
||||||
ALERT_FONT_BIG = 88
|
ALERT_FONT_BIG = 88
|
||||||
|
|
||||||
|
ALERT_HEIGHTS = {
|
||||||
|
AlertSize.small: 271,
|
||||||
|
AlertSize.mid: 420,
|
||||||
|
}
|
||||||
|
|
||||||
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
||||||
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
ALERT_COLORS = {
|
ALERT_COLORS = {
|
||||||
AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1
|
||||||
AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange
|
AlertStatus.userPrompt: rl.Color(0xDA, 0x6F, 0x25, 0xF1), # #DA6F25 with alpha 0xF1
|
||||||
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red
|
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -42,24 +48,24 @@ class Alert:
|
|||||||
|
|
||||||
# Pre-defined alert instances
|
# Pre-defined alert instances
|
||||||
ALERT_STARTUP_PENDING = Alert(
|
ALERT_STARTUP_PENDING = Alert(
|
||||||
text1="openpilot Unavailable",
|
text1=tr("openpilot Unavailable"),
|
||||||
text2="Waiting to start",
|
text2=tr("Waiting to start"),
|
||||||
size=AlertSize.mid,
|
size=AlertSize.mid,
|
||||||
status=AlertStatus.normal,
|
status=AlertStatus.normal,
|
||||||
)
|
)
|
||||||
|
|
||||||
ALERT_CRITICAL_TIMEOUT = Alert(
|
ALERT_CRITICAL_TIMEOUT = Alert(
|
||||||
text1="TAKE CONTROL IMMEDIATELY",
|
text1=tr("TAKE CONTROL IMMEDIATELY"),
|
||||||
text2="System Unresponsive",
|
text2=tr("System Unresponsive"),
|
||||||
size=AlertSize.full,
|
size=AlertSize.full,
|
||||||
status=AlertStatus.critical,
|
status=AlertStatus.critical,
|
||||||
)
|
)
|
||||||
|
|
||||||
ALERT_CRITICAL_REBOOT = Alert(
|
ALERT_CRITICAL_REBOOT = Alert(
|
||||||
text1="System Unresponsive",
|
text1=tr("System Unresponsive"),
|
||||||
text2="Reboot Device",
|
text2=tr("Reboot Device"),
|
||||||
size=AlertSize.full,
|
size=AlertSize.mid,
|
||||||
status=AlertStatus.critical,
|
status=AlertStatus.normal,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,14 +75,20 @@ class AlertRenderer(Widget):
|
|||||||
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
||||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
|
|
||||||
|
# font size is set dynamically
|
||||||
|
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
|
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
|
|
||||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||||
"""Generate the current alert based on selfdrive state."""
|
"""Generate the current alert based on selfdrive state."""
|
||||||
ss = sm['selfdriveState']
|
ss = sm['selfdriveState']
|
||||||
|
|
||||||
# Check if selfdriveState messages have stopped arriving
|
# Check if selfdriveState messages have stopped arriving
|
||||||
|
recv_frame = sm.recv_frame['selfdriveState']
|
||||||
if not sm.updated['selfdriveState']:
|
if not sm.updated['selfdriveState']:
|
||||||
recv_frame = sm.recv_frame['selfdriveState']
|
time_since_onroad = time.monotonic() - ui_state.started_time
|
||||||
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
|
|
||||||
|
|
||||||
# 1. Never received selfdriveState since going onroad
|
# 1. Never received selfdriveState since going onroad
|
||||||
waiting_for_startup = recv_frame < ui_state.started_frame
|
waiting_for_startup = recv_frame < ui_state.started_frame
|
||||||
@@ -95,13 +107,17 @@ class AlertRenderer(Widget):
|
|||||||
if ss.alertSize == 0:
|
if ss.alertSize == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Don't get old alert
|
||||||
|
if recv_frame < ui_state.started_frame:
|
||||||
|
return None
|
||||||
|
|
||||||
# Return current alert
|
# Return current alert
|
||||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw)
|
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw)
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
def _render(self, rect: rl.Rectangle):
|
||||||
alert = self.get_alert(ui_state.sm)
|
alert = self.get_alert(ui_state.sm)
|
||||||
if not alert:
|
if not alert:
|
||||||
return False
|
return
|
||||||
|
|
||||||
alert_rect = self._get_alert_rect(rect, alert.size)
|
alert_rect = self._get_alert_rect(rect, alert.size)
|
||||||
self._draw_background(alert_rect, alert)
|
self._draw_background(alert_rect, alert)
|
||||||
@@ -113,21 +129,14 @@ class AlertRenderer(Widget):
|
|||||||
alert_rect.height - 2 * ALERT_PADDING
|
alert_rect.height - 2 * ALERT_PADDING
|
||||||
)
|
)
|
||||||
self._draw_text(text_rect, alert)
|
self._draw_text(text_rect, alert)
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
||||||
if size == AlertSize.full:
|
if size == AlertSize.full:
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == AlertSize.small else
|
h = ALERT_HEIGHTS.get(size, rect.height)
|
||||||
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN,
|
||||||
|
rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2)
|
||||||
return rl.Rectangle(
|
|
||||||
rect.x + ALERT_MARGIN,
|
|
||||||
rect.y + rect.height - ALERT_MARGIN - height,
|
|
||||||
rect.width - 2 * ALERT_MARGIN,
|
|
||||||
height
|
|
||||||
)
|
|
||||||
|
|
||||||
def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None:
|
def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None:
|
||||||
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
||||||
@@ -150,13 +159,17 @@ class AlertRenderer(Widget):
|
|||||||
else:
|
else:
|
||||||
is_long = len(alert.text1) > 15
|
is_long = len(alert.text1) > 15
|
||||||
font_size1 = 132 if is_long else 177
|
font_size1 = 132 if is_long else 177
|
||||||
align_ment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER
|
|
||||||
vertical_align = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE
|
|
||||||
text_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height // 2)
|
|
||||||
|
|
||||||
gui_text_box(text_rect, alert.text1, font_size1, alignment=align_ment, alignment_vertical=vertical_align, font_weight=FontWeight.BOLD)
|
top_offset = 200 if is_long or '\n' in alert.text1 else 270
|
||||||
text_rect.y = rect.y + rect.height // 2
|
title_rect = rl.Rectangle(rect.x, rect.y + top_offset, rect.width, 600)
|
||||||
gui_text_box(text_rect, alert.text2, ALERT_FONT_BIG, alignment=align_ment)
|
self._full_text1_label.set_font_size(font_size1)
|
||||||
|
self._full_text1_label.set_text(alert.text1)
|
||||||
|
self._full_text1_label.render(title_rect)
|
||||||
|
|
||||||
|
bottom_offset = 361 if is_long else 420
|
||||||
|
subtitle_rect = rl.Rectangle(rect.x, rect.y + rect.height - bottom_offset, rect.width, 300)
|
||||||
|
self._full_text2_label.set_text(alert.text2)
|
||||||
|
self._full_text2_label.render(subtitle_rect)
|
||||||
|
|
||||||
def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None:
|
def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None:
|
||||||
text_size = measure_text_cached(font, text, font_size)
|
text_size = measure_text_cached(font, text, font_size)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from collections.abc import Callable
|
from cereal import log, messaging
|
||||||
from cereal import log
|
|
||||||
from msgq.visionipc import VisionStreamType
|
from msgq.visionipc import VisionStreamType
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
|
from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
|
||||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||||
@@ -20,13 +21,14 @@ WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD
|
|||||||
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
|
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
|
||||||
|
|
||||||
BORDER_COLORS = {
|
BORDER_COLORS = {
|
||||||
UIStatus.DISENGAGED: rl.Color(0x17, 0x33, 0x49, 0xC8), # Blue for disengaged state
|
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
|
||||||
UIStatus.OVERRIDE: rl.Color(0x91, 0x9B, 0x95, 0xF1), # Gray for override state
|
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
|
||||||
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
|
UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state
|
||||||
}
|
}
|
||||||
|
|
||||||
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
||||||
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
|
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
|
||||||
|
INF_POINT = np.array([1000.0, 0.0, 0.0])
|
||||||
|
|
||||||
|
|
||||||
class AugmentedRoadView(CameraView):
|
class AugmentedRoadView(CameraView):
|
||||||
@@ -38,9 +40,7 @@ class AugmentedRoadView(CameraView):
|
|||||||
self.view_from_calib = view_frame_from_device_frame.copy()
|
self.view_from_calib = view_frame_from_device_frame.copy()
|
||||||
self.view_from_wide_calib = view_frame_from_device_frame.copy()
|
self.view_from_wide_calib = view_frame_from_device_frame.copy()
|
||||||
|
|
||||||
self._last_calib_time: float = 0
|
self._matrix_cache_key = (0, 0.0, 0.0, stream_type)
|
||||||
self._last_rect_dims = (0.0, 0.0)
|
|
||||||
self._last_stream_type = stream_type
|
|
||||||
self._cached_matrix: np.ndarray | None = None
|
self._cached_matrix: np.ndarray | None = None
|
||||||
self._content_rect = rl.Rectangle()
|
self._content_rect = rl.Rectangle()
|
||||||
|
|
||||||
@@ -49,14 +49,12 @@ class AugmentedRoadView(CameraView):
|
|||||||
self.alert_renderer = AlertRenderer()
|
self.alert_renderer = AlertRenderer()
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
self.driver_state_renderer = DriverStateRenderer()
|
||||||
|
|
||||||
# Callbacks
|
# debug
|
||||||
self._click_callback: Callable | None = None
|
self._pm = messaging.PubMaster(['uiDebug'])
|
||||||
|
|
||||||
def set_callbacks(self, on_click: Callable | None = None):
|
|
||||||
self._click_callback = on_click
|
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
# Only render when system is started to avoid invalid data access
|
# Only render when system is started to avoid invalid data access
|
||||||
|
start_draw = time.monotonic()
|
||||||
if not ui_state.started:
|
if not ui_state.started:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -73,9 +71,6 @@ class AugmentedRoadView(CameraView):
|
|||||||
rect.height - 2 * UI_BORDER_SIZE,
|
rect.height - 2 * UI_BORDER_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Draw colored border based on driving state
|
|
||||||
self._draw_border(rect)
|
|
||||||
|
|
||||||
# Enable scissor mode to clip all rendering within content rectangle boundaries
|
# Enable scissor mode to clip all rendering within content rectangle boundaries
|
||||||
# This creates a rendering viewport that prevents graphics from drawing outside the border
|
# This creates a rendering viewport that prevents graphics from drawing outside the border
|
||||||
rl.begin_scissor_mode(
|
rl.begin_scissor_mode(
|
||||||
@@ -91,8 +86,8 @@ class AugmentedRoadView(CameraView):
|
|||||||
# Draw all UI overlays
|
# Draw all UI overlays
|
||||||
self.model_renderer.render(self._content_rect)
|
self.model_renderer.render(self._content_rect)
|
||||||
self._hud_renderer.render(self._content_rect)
|
self._hud_renderer.render(self._content_rect)
|
||||||
if not self.alert_renderer.render(self._content_rect):
|
self.alert_renderer.render(self._content_rect)
|
||||||
self.driver_state_renderer.render(self._content_rect)
|
self.driver_state_renderer.render(self._content_rect)
|
||||||
|
|
||||||
# Custom UI extension point - add custom overlays here
|
# Custom UI extension point - add custom overlays here
|
||||||
# Use self._content_rect for positioning within camera bounds
|
# Use self._content_rect for positioning within camera bounds
|
||||||
@@ -100,15 +95,29 @@ class AugmentedRoadView(CameraView):
|
|||||||
# End clipping region
|
# End clipping region
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
# Handle click events if no HUD interaction occurred
|
# Draw colored border based on driving state
|
||||||
if not self._hud_renderer.handle_mouse_event():
|
self._draw_border(rect)
|
||||||
if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
|
# publish uiDebug
|
||||||
self._click_callback()
|
msg = messaging.new_message('uiDebug')
|
||||||
|
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
|
||||||
|
self._pm.send('uiDebug', msg)
|
||||||
|
|
||||||
|
def _handle_mouse_press(self, _):
|
||||||
|
if not self._hud_renderer.user_interacting() and self._click_callback is not None:
|
||||||
|
self._click_callback()
|
||||||
|
|
||||||
|
def _handle_mouse_release(self, _):
|
||||||
|
# We only call click callback on press if not interacting with HUD
|
||||||
|
pass
|
||||||
|
|
||||||
def _draw_border(self, rect: rl.Rectangle):
|
def _draw_border(self, rect: rl.Rectangle):
|
||||||
|
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
|
||||||
|
border_roundness = 0.12
|
||||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
|
||||||
|
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
|
||||||
|
rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color)
|
||||||
|
|
||||||
def _switch_stream_if_needed(self, sm):
|
def _switch_stream_if_needed(self, sm):
|
||||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||||
@@ -151,12 +160,13 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
||||||
# Check if we can use cached matrix
|
# Check if we can use cached matrix
|
||||||
calib_time = ui_state.sm.recv_frame['liveCalibration']
|
cache_key = (
|
||||||
current_dims = (self._content_rect.width, self._content_rect.height)
|
ui_state.sm.recv_frame['liveCalibration'],
|
||||||
if (self._last_calib_time == calib_time and
|
self._content_rect.width,
|
||||||
self._last_rect_dims == current_dims and
|
self._content_rect.height,
|
||||||
self._last_stream_type == self.stream_type and
|
self.stream_type
|
||||||
self._cached_matrix is not None):
|
)
|
||||||
|
if cache_key == self._matrix_cache_key and self._cached_matrix is not None:
|
||||||
return self._cached_matrix
|
return self._cached_matrix
|
||||||
|
|
||||||
# Get camera configuration
|
# Get camera configuration
|
||||||
@@ -167,9 +177,8 @@ class AugmentedRoadView(CameraView):
|
|||||||
zoom = 2.0 if is_wide_camera else 1.1
|
zoom = 2.0 if is_wide_camera else 1.1
|
||||||
|
|
||||||
# Calculate transforms for vanishing point
|
# Calculate transforms for vanishing point
|
||||||
inf_point = np.array([1000.0, 0.0, 0.0])
|
|
||||||
calib_transform = intrinsic @ calibration
|
calib_transform = intrinsic @ calibration
|
||||||
kep = calib_transform @ inf_point
|
kep = calib_transform @ INF_POINT
|
||||||
|
|
||||||
# Calculate center points and dimensions
|
# Calculate center points and dimensions
|
||||||
x, y = self._content_rect.x, self._content_rect.y
|
x, y = self._content_rect.x, self._content_rect.y
|
||||||
@@ -192,9 +201,7 @@ class AugmentedRoadView(CameraView):
|
|||||||
x_offset, y_offset = 0, 0
|
x_offset, y_offset = 0, 0
|
||||||
|
|
||||||
# Cache the computed transformation matrix to avoid recalculations
|
# Cache the computed transformation matrix to avoid recalculations
|
||||||
self._last_calib_time = calib_time
|
self._matrix_cache_key = cache_key
|
||||||
self._last_rect_dims = current_dims
|
|
||||||
self._last_stream_type = self.stream_type
|
|
||||||
self._cached_matrix = np.array([
|
self._cached_matrix = np.array([
|
||||||
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
|
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
|
||||||
[0, zoom * 2 * cy / h, -y_offset / h * 2],
|
[0, zoom * 2 * cy / h, -y_offset / h * 2],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from openpilot.system.hardware import TICI
|
|||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ else:
|
|||||||
class CameraView(Widget):
|
class CameraView(Widget):
|
||||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
# TODO: implement a receiver and connect thread
|
||||||
self._name = name
|
self._name = name
|
||||||
# Primary stream
|
# Primary stream
|
||||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||||
@@ -103,6 +105,20 @@ class CameraView(Widget):
|
|||||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||||
rl.unload_image(temp_image)
|
rl.unload_image(temp_image)
|
||||||
|
|
||||||
|
ui_state.add_offroad_transition_callback(self._offroad_transition)
|
||||||
|
|
||||||
|
def _offroad_transition(self):
|
||||||
|
# Reconnect if not first time going onroad
|
||||||
|
if ui_state.is_onroad() and self.frame is not None:
|
||||||
|
# Prevent old frames from showing when going onroad. Qt has a separate thread
|
||||||
|
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
|
||||||
|
# and only clears internal buffers, not the message queue.
|
||||||
|
self.frame = None
|
||||||
|
self.available_streams.clear()
|
||||||
|
if self.client:
|
||||||
|
del self.client
|
||||||
|
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
|
||||||
|
|
||||||
def _set_placeholder_color(self, color: rl.Color):
|
def _set_placeholder_color(self, color: rl.Color):
|
||||||
"""Set a placeholder color to be drawn when no frame is available."""
|
"""Set a placeholder color to be drawn when no frame is available."""
|
||||||
self._placeholder_color = color
|
self._placeholder_color = color
|
||||||
@@ -139,6 +155,8 @@ class CameraView(Widget):
|
|||||||
if self.shader and self.shader.id:
|
if self.shader and self.shader.id:
|
||||||
rl.unload_shader(self.shader)
|
rl.unload_shader(self.shader)
|
||||||
|
|
||||||
|
self.frame = None
|
||||||
|
self.available_streams.clear()
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@@ -175,6 +193,9 @@ class CameraView(Widget):
|
|||||||
if buffer:
|
if buffer:
|
||||||
self._texture_needs_update = True
|
self._texture_needs_update = True
|
||||||
self.frame = buffer
|
self.frame = buffer
|
||||||
|
elif not self.client.is_connected():
|
||||||
|
# ensure we clear the displayed frame when the connection is lost
|
||||||
|
self.frame = None
|
||||||
|
|
||||||
if not self.frame:
|
if not self.frame:
|
||||||
self._draw_placeholder(rect)
|
self._draw_placeholder(rect)
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import pyray as rl
|
|||||||
from msgq.visionipc import VisionStreamType
|
from msgq.visionipc import VisionStreamType
|
||||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
|
|
||||||
|
|
||||||
@@ -12,17 +13,25 @@ class DriverCameraDialog(CameraView):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
self.driver_state_renderer = DriverStateRenderer()
|
||||||
|
# TODO: this can grow unbounded, should be given some thought
|
||||||
|
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
|
||||||
|
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||||
|
|
||||||
|
def stop_dmonitoringmodeld(self):
|
||||||
|
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||||
|
gui_app.set_modal_overlay(None)
|
||||||
|
|
||||||
|
def _handle_mouse_release(self, _):
|
||||||
|
super()._handle_mouse_release(_)
|
||||||
|
self.stop_dmonitoringmodeld()
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
super()._render(rect)
|
super()._render(rect)
|
||||||
|
|
||||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if not self.frame:
|
if not self.frame:
|
||||||
gui_label(
|
gui_label(
|
||||||
rect,
|
rect,
|
||||||
"camera starting",
|
tr("camera starting"),
|
||||||
font_size=100,
|
font_size=100,
|
||||||
font_weight=FontWeight.BOLD,
|
font_weight=FontWeight.BOLD,
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user