mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-02 12:03:56 +08:00
Compare commits
588 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a3b5e2a5 | |||
| 8ac6b224d7 | |||
| d43477ec41 | |||
| 0f981ad88f | |||
| dbd28f84c4 | |||
| f95f5a88cf | |||
| 5bef40f51c | |||
| 6b9b8a4dfb | |||
| 823f6923c8 | |||
| 9e6dabb149 | |||
| 9712480382 | |||
| e5265f88b9 | |||
| 4a9881ff5b | |||
| b00ca4c52e | |||
| 25cbb14cbd | |||
| 1ebbff5cd9 | |||
| 163751cb7e | |||
| 91b378cc5b | |||
| 3f693e1735 | |||
| d646a5dea8 | |||
| 0b54917f90 | |||
| e15bd6bf3d | |||
| baf9a3fff6 | |||
| 3b081b2fc2 | |||
| 24126eafdc | |||
| 44efc8d935 | |||
| 5dddf9ae1d | |||
| b863214df8 | |||
| 6efe2e5379 | |||
| 04c590f7c4 | |||
| 39d30d89c1 | |||
| 567bde2b4e | |||
| 4f4cb4e7d9 | |||
| 15dc2cb26c | |||
| 317f08d15a | |||
| c0ae7127ef | |||
| b50b5cc1a3 | |||
| e9d39b4f5e | |||
| d5700b6910 | |||
| b0c054d881 | |||
| d4dbb83a55 | |||
| b6b820a275 | |||
| d8a218b807 | |||
| 0a2ee300e7 | |||
| 1996510d7a | |||
| a8e10e4e52 | |||
| a5f43c1d4b | |||
| a25dcb535a | |||
| 0172b49987 | |||
| c5ae97f216 | |||
| d2f3acc248 | |||
| 976d2a2222 | |||
| a67202cfb4 | |||
| 4194390dc1 | |||
| 276f3561ce | |||
| 7afcf3a8ac | |||
| 7403185bd8 | |||
| df22d36413 | |||
| 656ba431e9 | |||
| 50fc142dd7 | |||
| 2a7ebf190c | |||
| c276b589c7 | |||
| 04b8eb0cd3 | |||
| 806783c148 | |||
| c7e6c27eda | |||
| 1a7c20ab6c | |||
| d2d2399609 | |||
| 0cc30a9b30 | |||
| 1e7d9c311f | |||
| 0eec2e2a55 | |||
| 748e7b13fc | |||
| 1f17185669 | |||
| 6fae1942e4 | |||
| 094332da2c | |||
| 16eaf005fe | |||
| 12601d1fac | |||
| 0022fde1cb | |||
| 332540a931 | |||
| d28aa489ca | |||
| a5eda8a63a | |||
| 6e5879e035 | |||
| d377f5efd7 | |||
| db297a341b | |||
| 091682eb06 | |||
| c779fc4672 | |||
| 26bc78b69d | |||
| 3a1d0bc615 | |||
| 7897829d9b | |||
| 1353dc4943 | |||
| f50aaa1d6d | |||
| 076cda2d6c | |||
| 37dc1533ef | |||
| f9bf710396 | |||
| 04030a447e | |||
| eb955e391c | |||
| 95d87910f9 | |||
| 4b54f61deb | |||
| c14afb153e | |||
| 5755042c96 | |||
| e1c73f2359 | |||
| 0c03e1b1b8 | |||
| 63fffaa27b | |||
| eac8cea6ee | |||
| 4b5a99e66d | |||
| 0a7529919f | |||
| 77f54a22f0 | |||
| 1887a45a68 | |||
| d45c1f2b1b | |||
| 2f68bdedca | |||
| dcb7d5de82 | |||
| 657a9462ce | |||
| 8da7e4f493 | |||
| db24c48edc | |||
| 01a31c1f2f | |||
| 9d2d8f3a31 | |||
| 7df9af125d | |||
| dc9a5743ee | |||
| 165651b096 | |||
| a75d3fd787 | |||
| b00a235274 | |||
| b0c6319a1a | |||
| 2993c8fb07 | |||
| c7e47a0289 | |||
| 733253c7c5 | |||
| f487123e4c | |||
| 131739bda6 | |||
| 90ca300382 | |||
| 9d50b3734c | |||
| 7fe4b5b550 | |||
| 5961cbd675 | |||
| f19c719d76 | |||
| 6e72874e55 | |||
| dc2b45ef1e | |||
| 9b1ebf0635 | |||
| 4d203961da | |||
| 7c25601821 | |||
| 09dbb29d5a | |||
| dc86e03e60 | |||
| da51312a6f | |||
| b98d9a1e6e | |||
| 18b6a4a458 | |||
| 6d201df86a | |||
| 43007a6318 | |||
| 80ca622ea6 | |||
| 266ee4ce07 | |||
| b46431793b | |||
| c06d1dfe16 | |||
| 2d2f662bdf | |||
| ed3f88ef52 | |||
| 29ab44fc4d | |||
| b42499038d | |||
| 5dea258223 | |||
| 7d2c0aafea | |||
| 92f0c26228 | |||
| 3b22e29c20 | |||
| 206907f651 | |||
| e0bea55fa9 | |||
| 7c795af543 | |||
| 8b9338a0a6 | |||
| 9a2d62445d | |||
| 314c92e51f | |||
| af84074472 | |||
| 4a38356a5f | |||
| ecc1db481b | |||
| 6eb9df9642 | |||
| 8c814777f6 | |||
| 9e6a49742e | |||
| 359549db26 | |||
| 468770cabd | |||
| 1ec8ca7abb | |||
| d049218089 | |||
| 7a8e17dda5 | |||
| d48e34a3a0 | |||
| 6f4fdfb851 | |||
| 7011142a49 | |||
| d048a8eb98 | |||
| 22b2f2a898 | |||
| 4e74d8890d | |||
| 7e2cf618e0 | |||
| 1f8205231f | |||
| 4bee14a4d7 | |||
| 68629dcb82 | |||
| 29da914430 | |||
| c4c5d276d5 | |||
| a012e58a99 | |||
| c8d2a8aaf0 | |||
| b2296b4844 | |||
| 403e360551 | |||
| c1d966997d | |||
| 143b9b5d4f | |||
| 6053d3e6ff | |||
| 9e08f0b1bb | |||
| 1e189a1192 | |||
| 84fb671740 | |||
| 86b752f9d2 | |||
| 2fe363a1f8 | |||
| 989beaae3c | |||
| d6273c7137 | |||
| 1a2cd5e55d | |||
| 3149512d97 | |||
| edb618e910 | |||
| 482e3cd7f2 | |||
| 687bc6835d | |||
| aaa2f687ea | |||
| c9fbbc0331 | |||
| e24c677109 | |||
| 996542ffef | |||
| dbc5af5d55 | |||
| f99d84daa8 | |||
| b98f1bfce5 | |||
| 1fdc8b89e8 | |||
| da6e0bd0a9 | |||
| c642fd9427 | |||
| 0147c90469 | |||
| 4e956b46ec | |||
| 107e99a5fc | |||
| 7610f33129 | |||
| 5acadf1d91 | |||
| 33ca04f895 | |||
| 0171603f6d | |||
| cac674d270 | |||
| faa890e903 | |||
| c7fe28fdfd | |||
| cc9161776a | |||
| 8e1bb6995c | |||
| 1ad498846f | |||
| fe5b2c66da | |||
| f7ca352105 | |||
| 8a3e80cddd | |||
| d429cc3c90 | |||
| 6095ea559c | |||
| cdb7e7a5d4 | |||
| 95155234e8 | |||
| de94ce4ad8 | |||
| ab2baa98ea | |||
| 4e46321160 | |||
| 15a78b9b33 | |||
| a57cd3d54a | |||
| 82f3baaebb | |||
| 6dff9ba47f | |||
| 9e191161d7 | |||
| 011e46b8ed | |||
| 9ce2893646 | |||
| 4d8467ce37 | |||
| c6a366b013 | |||
| d921cf6559 | |||
| 15578da372 | |||
| 6148a86dc1 | |||
| a0fd5ea025 | |||
| 16509c3803 | |||
| 0f1cce2285 | |||
| 83a90e89b4 | |||
| 82367fd413 | |||
| ca2373a9a0 | |||
| d4027487c0 | |||
| 9cf41c7b38 | |||
| f9a06b7d1a | |||
| dc1e248d03 | |||
| 2d58df950b | |||
| 9983d3a4cb | |||
| 08c0efb01d | |||
| 8adbeddcbb | |||
| 2c352b90b4 | |||
| 0852af76e3 | |||
| d747e5ca08 | |||
| 6633870fb3 | |||
| a9edd2ca3b | |||
| 4840e9e1d9 | |||
| 94f88b41a9 | |||
| 2077b98d76 | |||
| 8bd7ac53c7 | |||
| 332d235693 | |||
| 7aea14397c | |||
| 951947cef2 | |||
| 3f06976373 | |||
| 26025d80dd | |||
| e18bf451d0 | |||
| 84087746d7 | |||
| 6a8c1cf8cf | |||
| ec957d6430 | |||
| 27c7ba7827 | |||
| 194d6fc106 | |||
| 23ca75140a | |||
| d0a90d10ef | |||
| 778eed3a60 | |||
| 62dc081f70 | |||
| 204a6f8534 | |||
| 4413c29923 | |||
| 43360170dd | |||
| 281f5b6b3a | |||
| 9ec0303f93 | |||
| bb51c66589 | |||
| 8c5f4bae13 | |||
| 9c5da263cc | |||
| 8025957925 | |||
| d4a49cb8c1 | |||
| bb39b9f388 | |||
| 7926a909e1 | |||
| b4acca0798 | |||
| c7cc700e4e | |||
| 89509ba3c7 | |||
| 843c364494 | |||
| b5a58c741f | |||
| f702c707e5 | |||
| ce980ed98f | |||
| 5814e26a53 | |||
| 2cc8849544 | |||
| fb6b75a81d | |||
| 7f584d7e58 | |||
| c777a9ec8a | |||
| f63e715d2d | |||
| e11ce3071d | |||
| a531afd02a | |||
| a57a08843a | |||
| 3c592b58b0 | |||
| b781fa90d7 | |||
| 59c29e6e9c | |||
| ba906bb038 | |||
| ab1aeda540 | |||
| 46051ce2b4 | |||
| 5ac0bbab48 | |||
| 236fd3942f | |||
| c6db508304 | |||
| 3305afdf98 | |||
| 08d38e9bdf | |||
| c8bbe72a2a | |||
| 57bfa98906 | |||
| 38bee61910 | |||
| 9748e4a449 | |||
| ea61d3002c | |||
| 3f8f1c69e4 | |||
| 7ac512caf2 | |||
| e3070b7b42 | |||
| de28764611 | |||
| 3366652afa | |||
| 928aeb8fe2 | |||
| 40afc1bad2 | |||
| ecc570756a | |||
| a7c78e97b0 | |||
| 9b1bbf7d21 | |||
| e70f7ea88b | |||
| e72e3a875b | |||
| db741fd789 | |||
| 2325f052f0 | |||
| b41fdc6545 | |||
| c03714aab2 | |||
| b79ea58759 | |||
| 66f0ec1371 | |||
| 40e84d4a54 | |||
| 057b881da8 | |||
| 7924313af9 | |||
| 7ebd1b46a7 | |||
| 6a9df653ab | |||
| 036d2f0fee | |||
| d0a252d34b | |||
| 11b26d3511 | |||
| 17f3920a55 | |||
| 456c1955a1 | |||
| 744032b961 | |||
| 6a773e7d2d | |||
| f87e6e4093 | |||
| 4c7854dea8 | |||
| 45a73a439b | |||
| 8462a39b65 | |||
| 29546c710a | |||
| d8d39d1b35 | |||
| 178c8694f2 | |||
| 2985ed9f6d | |||
| bd764bbf2e | |||
| 8e27decb08 | |||
| 1382b62b00 | |||
| ffaa466852 | |||
| 0c0d4bc8cb | |||
| c78aeed361 | |||
| c145bf602f | |||
| 7e19d85b8b | |||
| 4523d17ecd | |||
| 9306f865e8 | |||
| 3464d40526 | |||
| b51b8dafd6 | |||
| 46c752caea | |||
| 01479b56b9 | |||
| a64dd65025 | |||
| 838e93b934 | |||
| b88e2eccb9 | |||
| 86db7a63ba | |||
| 6120da2d54 | |||
| 5277fc139f | |||
| cef4eec062 | |||
| 2d10707628 | |||
| d0400456cf | |||
| 872de3b085 | |||
| e6f7d9c473 | |||
| e57c9a5989 | |||
| deefc26c85 | |||
| 1f66903899 | |||
| 98a6ca21f3 | |||
| a3b270cbfc | |||
| 3ce8e82c73 | |||
| 0970a7b685 | |||
| 36ecda9f25 | |||
| 0cdc3287b5 | |||
| d69a9f4db5 | |||
| 9ab83d8d59 | |||
| f8c91e6c62 | |||
| 86be96c44c | |||
| 82f9995887 | |||
| 9af5e64787 | |||
| 075b73e940 | |||
| e914d761b1 | |||
| f33c8605da | |||
| 155b105a44 | |||
| ce66b2ed7d | |||
| d36a4b8888 | |||
| fb943dd85a | |||
| 371000a848 | |||
| 33952c42b9 | |||
| 7731bee396 | |||
| 74d2ef9698 | |||
| ccdd137671 | |||
| 5480ae3caa | |||
| 2b42135302 | |||
| 8f2e33dead | |||
| 91e1ce8466 | |||
| ee83b3010a | |||
| d773f87897 | |||
| d2dbeddcf1 | |||
| aab66cd015 | |||
| b151ca11cb | |||
| 21c74f72ef | |||
| 4b9de265bf | |||
| 70799d5bc3 | |||
| a5f18d35cd | |||
| 9ab16c0838 | |||
| 51dd8d48ce | |||
| 55a40280fb | |||
| f964651e60 | |||
| 4366e6ac7a | |||
| 55bbf6b98a | |||
| 77347170f8 | |||
| d77bea5972 | |||
| 1973ad6439 | |||
| 44beeb7606 | |||
| 4854bc74b6 | |||
| 3d2d094c8b | |||
| 0972fec4ef | |||
| ab8368f287 | |||
| 0299391cf8 | |||
| db73e3bdbb | |||
| 7edb10e149 | |||
| 4f3110af3a | |||
| c7c3dbd699 | |||
| 58a4514321 | |||
| 4b3017a7fc | |||
| 8e6cd917e4 | |||
| ab9b2d7624 | |||
| 5ffbe27aaa | |||
| 66a573747d | |||
| 0c1f0b3d8d | |||
| b3c26b5824 | |||
| 43eea2e977 | |||
| 3c687b55f7 | |||
| 3372a71b4a | |||
| 9632656c59 | |||
| 5a2e9d2606 | |||
| 1477a59fcd | |||
| 8090e48441 | |||
| 297f56f29a | |||
| 4fff2bba47 | |||
| 4f1b668aea | |||
| 1319c60fda | |||
| 223c4f1322 | |||
| d3c0e9df8b | |||
| e9abadd2f4 | |||
| 9de201d18d | |||
| b300093058 | |||
| 97a50dead6 | |||
| 210e6504fe | |||
| 31d888ba79 | |||
| 5ee8d662a7 | |||
| c569a798ba | |||
| f334ac0693 | |||
| defaf64c0e | |||
| ba6eac264e | |||
| 4ed9546240 | |||
| 1cbf3b927e | |||
| a4e1631228 | |||
| 21ee77ea30 | |||
| fc6d6604aa | |||
| cb85a84e30 | |||
| 708a68adeb | |||
| 67c1fb6a64 | |||
| fc2a89b988 | |||
| f1434fe34d | |||
| da628907d0 | |||
| 5748148e7c | |||
| 8824bea044 | |||
| 2fb333f279 | |||
| 4000b44866 | |||
| 10be50fd7c | |||
| ad84d01235 | |||
| ad3fa91b09 | |||
| 01b06dcc71 | |||
| 409601216b | |||
| d7db526f8c | |||
| 4b1466f5eb | |||
| 51646bf4cb | |||
| 6ca072af44 | |||
| ea916e50bd | |||
| 5cf5637d65 | |||
| 2abc15c222 | |||
| 42d0beabee | |||
| f5f8a331f1 | |||
| a278aa8fba | |||
| ec856f38e6 | |||
| f0f9b43bef | |||
| 0b01007a9b | |||
| b9a8a41784 | |||
| 2ad95d4176 | |||
| e26202eae2 | |||
| 4da8dd6c3f | |||
| c0b16fe8cc | |||
| 7775d42443 | |||
| ab34e5d8a5 | |||
| 12b6dadf17 | |||
| 7ea822bbb1 | |||
| 86aa912748 | |||
| 3c7898921b | |||
| f6a03dce08 | |||
| 31c5010e11 | |||
| 7ea5058365 | |||
| 7c9b45bb5b | |||
| f4b8982ee0 | |||
| 470ab5cc70 | |||
| cdcf6a0dd1 | |||
| 28a017e12e | |||
| 2a73dc50ff | |||
| bd490d2ee3 | |||
| edb2be5716 | |||
| f5c56bc631 | |||
| fc753b3900 | |||
| b801a9ff42 | |||
| 764b94f2ab | |||
| de0092349d | |||
| b209e6a655 | |||
| d2db94e177 | |||
| cfaf5b05b2 | |||
| fdcf1db290 | |||
| 68f0dde601 | |||
| 19e0619ae9 | |||
| 12d77dd2d6 | |||
| 7d14e28c4d | |||
| 749dba88f5 | |||
| 6bc930a07b | |||
| 219dc02f11 | |||
| 37bd6981ad | |||
| ded5368547 | |||
| aeb33f3ae7 | |||
| 9f2830b94b | |||
| b07d3f94e2 | |||
| afd4cde972 | |||
| d2cc8e282e | |||
| 18505b6af7 | |||
| 223f1869eb | |||
| c871086830 | |||
| 662e152627 | |||
| b432b14a3b | |||
| 1f733a74ef | |||
| 2919499c61 | |||
| 9deea7d855 | |||
| 73c3ddd45c | |||
| e96ba4205f | |||
| 40abb31b4d | |||
| a8d121bf45 | |||
| 786a1981b6 | |||
| 8f628ba48b | |||
| 5c27cc667b | |||
| 9685faaed1 | |||
| 11a3272049 | |||
| d9bd0a06a9 | |||
| 651fc56ae6 | |||
| 63f6655dce | |||
| 2cb5b05746 | |||
| cda3693dd8 | |||
| f23e0449ff | |||
| 722ce72d33 | |||
| 7f1196d00e | |||
| 5b9713a042 |
@@ -12,6 +12,12 @@ openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl -text
|
||||
*.woff2 -text
|
||||
*.wav -text
|
||||
openpilot/starpilot/galaxy/web/vendor/vue/vue.esm-browser.js -text
|
||||
*.apk -text
|
||||
openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 -text
|
||||
openpilot/starpilot/soloist/pulseaudio/bin/pulseaudio -text
|
||||
openpilot/starpilot/soloist/pulseaudio/bin/pactl -text
|
||||
openpilot/starpilot/soloist/bluealsa/bin/bluealsa-aplay -text
|
||||
openpilot/starpilot/soloist/bluealsa/bin/bluealsad -text
|
||||
|
||||
openpilot/selfdrive/car/tests/test_models_segs.txt -text
|
||||
openpilot/common/hardware/comma/updater -text
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Android companion release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: android-companion-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test, build and sign the companion APK
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment: android-release
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openpilot/starpilot/companion/android
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false
|
||||
lfs: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
- name: Install Android SDK platform and build tools
|
||||
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-37" "build-tools;37.0.0"
|
||||
- name: Unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
- name: Restore signing key
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.COMPANION_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
umask 077
|
||||
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/companion.jks"
|
||||
- name: Build and sign the APK
|
||||
env:
|
||||
COMPANION_KEYSTORE: ${{ runner.temp }}/companion.jks
|
||||
COMPANION_KEY_PASSWORD: ${{ secrets.COMPANION_KEY_PASSWORD }}
|
||||
run: ./build_apk.sh
|
||||
- name: Remove signing key
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/companion.jks"
|
||||
- name: Check the APK
|
||||
working-directory: openpilot/starpilot/galaxy/bin
|
||||
run: |
|
||||
if unzip -l starpilot-companion.apk | grep -q 'assets/galaxy/'; then
|
||||
echo 'APK still bundles Galaxy' >&2
|
||||
exit 1
|
||||
fi
|
||||
BT="$ANDROID_HOME/build-tools/37.0.0"
|
||||
"$BT/aapt2" dump badging starpilot-companion.apk | head -1
|
||||
"$BT/apksigner" verify --print-certs starpilot-companion.apk | grep -E 'certificate (DN|SHA-256)'
|
||||
cat starpilot-companion.apk.version
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: starpilot-companion-apk
|
||||
path: |
|
||||
openpilot/starpilot/galaxy/bin/starpilot-companion.apk
|
||||
openpilot/starpilot/galaxy/bin/starpilot-companion.apk.version
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -29,11 +29,10 @@ jobs:
|
||||
steps:
|
||||
- uses: commaai/timeout@v1
|
||||
- uses: actions/checkout@v7
|
||||
- run: ./tools/op.sh setup
|
||||
|
||||
# Build
|
||||
- name: Build docs
|
||||
run: ./tools/op.sh docs --build
|
||||
run: python3 docs/serve.py --build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
name: iOS companion
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [SecretGoodStarPilot, secretgood-ios-integration]
|
||||
paths:
|
||||
- 'openpilot/starpilot/companion/ios/**'
|
||||
- 'openpilot/starpilot/galaxy_bluetooth/**'
|
||||
- '.github/workflows/ios-companion.yaml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openpilot/starpilot/companion/ios/**'
|
||||
- 'openpilot/starpilot/galaxy_bluetooth/**'
|
||||
- '.github/workflows/ios-companion.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ios-companion-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
app:
|
||||
name: Swift tests and iOS Simulator build
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 25
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openpilot/starpilot/companion/ios
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: false
|
||||
lfs: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Run companion regressions
|
||||
env:
|
||||
CI_PRIMARY_REPOSITORY_PATH: ${{ github.workspace }}/openpilot/starpilot/companion/ios
|
||||
run: sh ci_scripts/ci_post_clone.sh
|
||||
- name: Build for iOS Simulator
|
||||
run: >-
|
||||
xcodebuild -project GalaxyBluetooth.xcodeproj
|
||||
-scheme GalaxyBluetooth -configuration Debug
|
||||
-sdk iphonesimulator -destination 'generic/platform=iOS Simulator'
|
||||
-derivedDataPath "$RUNNER_TEMP/galaxy-ios-build"
|
||||
CODE_SIGNING_ALLOWED=NO build
|
||||
|
||||
bridge:
|
||||
name: Comma Bluetooth bridge tests
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: false
|
||||
lfs: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install bridge test dependencies
|
||||
run: python -m pip install pycryptodome
|
||||
- name: Run bridge regressions
|
||||
run: python -m unittest discover -s openpilot/starpilot/galaxy_bluetooth/tests -v
|
||||
@@ -34,6 +34,11 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Validate published device build receipt
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/SecretGoodStarPilot'
|
||||
run: python3 tools/ci/check_prebuilt_receipt.py --revision HEAD
|
||||
- name: Check shipped device runtime dependencies
|
||||
run: python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
|
||||
- name: Validate tracked dependencies
|
||||
run: python3 tools/vendor/check.py --revision HEAD
|
||||
- run: ./tools/op.sh setup
|
||||
@@ -196,13 +201,8 @@ jobs:
|
||||
timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }}
|
||||
env:
|
||||
RAYLIB_BACKEND: headless
|
||||
STARPILOT_TEST_DIAGNOSTICS: ci-results/host-test-diagnostics/unittest
|
||||
run: tools/op.sh test --json-output unit-test-results.json
|
||||
- name: Test custom module-level pytest cases
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
RAYLIB_BACKEND: headless
|
||||
run: python3 tools/ci/run_host_tests.py --pytest
|
||||
- name: Test GM vehicle configurations and CAN behavior
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 5
|
||||
@@ -227,13 +227,139 @@ jobs:
|
||||
STARPILOT_PYTHON: ${{ github.workspace }}/.venv/bin/python
|
||||
run: go test -mod=readonly -race -run '^Test(MsgqNativeLayout|Shadow(GoPythonIPC|ExecutableHostGpsIPC))$' -count=1 -v
|
||||
- name: Save complete unit test results
|
||||
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '') }}
|
||||
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-test-results
|
||||
path: |
|
||||
unit-test-results.json
|
||||
gm-test-results.json
|
||||
ci-results/host-test-diagnostics/
|
||||
if-no-files-found: error
|
||||
|
||||
custom_pytest:
|
||||
name: custom pytest (${{ matrix.shard }})
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Provide JavaScript runtime for Galaxy contract tests
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24'
|
||||
package-manager-cache: false
|
||||
- name: Provide pinned map provider toolchain
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: '1.25.1'
|
||||
cache-dependency-path: mapd_repo/go.sum
|
||||
- name: Fetch pinned map provider dependencies
|
||||
working-directory: mapd_repo
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
run: go mod download
|
||||
- run: ./tools/op.sh setup
|
||||
id: setup
|
||||
- name: Build openpilot
|
||||
id: build
|
||||
run: scons
|
||||
- name: Provide H.264 encoder for Android Auto integration tests
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
run: |
|
||||
uv pip install --python .venv/bin/python --only-binary av av==19.0.1
|
||||
.venv/bin/python -c "import av; assert 'libx264' in av.codecs_available"
|
||||
- name: Test every assigned custom pytest node serially
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
RAYLIB_BACKEND: headless
|
||||
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}"
|
||||
- name: Save collection, every outcome, JUnit and durable worker diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: custom-pytest-${{ matrix.shard }}
|
||||
path: ci-results/custom-pytest-${{ matrix.shard }}/
|
||||
if-no-files-found: error
|
||||
|
||||
custom_pytest_complete:
|
||||
name: complete custom pytest catalog
|
||||
needs: custom_pytest
|
||||
if: ${{ always() && !cancelled() }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: custom-pytest-*
|
||||
path: ci-results/custom-pytest-shards
|
||||
- name: Require every case passed or audited capability-unavailable
|
||||
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json
|
||||
- name: Save complete custom catalog verdict
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: custom-pytest-complete
|
||||
path: ci-results/custom-pytest-complete.json
|
||||
if-no-files-found: error
|
||||
|
||||
aol_lifecycles:
|
||||
name: AOL lifecycles (${{ matrix.shard }})
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
env:
|
||||
FUZZ_SEED: '0'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build native host dependencies
|
||||
run: scons
|
||||
- name: Gate every assigned lifecycle module serially
|
||||
timeout-minutes: 10
|
||||
run: python3 tools/ci/run_vehicle_tests.py --suite aol-lifecycle --shard-index "${{ matrix.shard }}" --shard-count 16 --output "ci-results/aol-lifecycle-${{ matrix.shard }}"
|
||||
- name: Save every lifecycle outcome and native build
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aol-lifecycle-${{ matrix.shard }}
|
||||
path: ci-results/aol-lifecycle-${{ matrix.shard }}/
|
||||
if-no-files-found: error
|
||||
|
||||
aol_lifecycle_complete:
|
||||
name: complete AOL lifecycle catalog
|
||||
needs: aol_lifecycles
|
||||
if: ${{ always() && !cancelled() }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: aol-lifecycle-*
|
||||
path: ci-results/aol-lifecycles
|
||||
- name: Require every full-catalog case exactly once and passed
|
||||
run: python3 tools/ci/lifecycle_shards.py ci-results/aol-lifecycles --count 16 --output ci-results/aol-lifecycle-complete.json
|
||||
- name: Save complete catalog verdict
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aol-lifecycle-complete
|
||||
path: ci-results/aol-lifecycle-complete.json
|
||||
if-no-files-found: error
|
||||
|
||||
map_provider:
|
||||
@@ -269,7 +395,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
suite: [interfaces, safety-debug, safety-release]
|
||||
suite: [interfaces, safety-debug]
|
||||
env:
|
||||
FUZZ_SEED: '0'
|
||||
steps:
|
||||
@@ -295,6 +421,57 @@ jobs:
|
||||
path: ci-results/${{ matrix.suite }}/
|
||||
if-no-files-found: error
|
||||
|
||||
safety_release_contracts:
|
||||
name: RELEASE vehicle contracts (${{ matrix.shard }})
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2, 3]
|
||||
env:
|
||||
FUZZ_SEED: '0'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build native host dependencies
|
||||
run: scons
|
||||
- name: Run every assigned RELEASE target serially
|
||||
timeout-minutes: 20
|
||||
run: python3 tools/ci/run_vehicle_tests.py --suite safety-release --shard-index "${{ matrix.shard }}" --shard-count 4 --output "ci-results/safety-release-${{ matrix.shard }}"
|
||||
- name: Save every RELEASE outcome and native build
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: safety-release-${{ matrix.shard }}
|
||||
path: ci-results/safety-release-${{ matrix.shard }}/
|
||||
if-no-files-found: error
|
||||
|
||||
safety_release_complete:
|
||||
name: complete RELEASE vehicle catalog
|
||||
needs: safety_release_contracts
|
||||
if: ${{ always() && !cancelled() }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: safety-release-*
|
||||
path: ci-results/safety-release-shards
|
||||
- name: Require every RELEASE case exactly once and passed
|
||||
run: python3 tools/ci/lifecycle_shards.py ci-results/safety-release-shards --suite safety-release --count 4 --output ci-results/safety-release-complete.json
|
||||
- name: Save complete RELEASE verdict
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: safety-release-complete
|
||||
path: ci-results/safety-release-complete.json
|
||||
if-no-files-found: error
|
||||
|
||||
recorded_vehicles:
|
||||
name: recorded vehicles (${{ matrix.variant }})
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -398,6 +575,15 @@ jobs:
|
||||
with:
|
||||
name: process_replay_diff.txt
|
||||
path: openpilot/selfdrive/test/process_replay/diff.txt
|
||||
- name: Save complete replay outputs for review
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: process-replay-outputs
|
||||
path: |
|
||||
openpilot/selfdrive/test/process_replay/fakedata/*.zst
|
||||
openpilot/selfdrive/test/process_replay/coverage.json
|
||||
if-no-files-found: error
|
||||
- name: Upload diff report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
|
||||
@@ -18,6 +18,7 @@ a.out
|
||||
.venv-linux-arm64/
|
||||
bin/
|
||||
!openpilot/starpilot/galaxy/bin/
|
||||
!openpilot/starpilot/soloist/pulseaudio/bin/
|
||||
|
||||
# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml)
|
||||
/msgq
|
||||
@@ -44,6 +45,8 @@ bin/
|
||||
*.os-*
|
||||
*.so
|
||||
!openpilot/tools/replay/libmedia_mux.so
|
||||
!openpilot/starpilot/soloist/pulseaudio/lib/*.so
|
||||
!openpilot/starpilot/soloist/pulseaudio/modules/*.so
|
||||
*.a
|
||||
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
|
||||
*.unchunked
|
||||
@@ -138,3 +141,5 @@ openpilot/selfdrive/modeld/models/*.unchunked
|
||||
/jetlink_repo/jetlink.egg-info/
|
||||
|
||||
# Local Google Play resolver build output (source and lockfile are versioned).
|
||||
|
||||
openpilot/starpilot/flm/_primitive_walk.cpp
|
||||
|
||||
@@ -25,3 +25,11 @@ The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and For
|
||||
## RAV4 Continental ARS510 receiver
|
||||
|
||||
The optional ACC-target and fused-track receivers and DBC preserve firestar5683's Dom integration from `b220a37f4bd119e402225a7aa16a7cd84878d0af` (Sea Turtles, 2026-10-13) and its ARS510 source attribution to ds-sebastian/ars510-radar `fb5b29c2b5999f34afeb6f7ac494a73949d66c67`, copyright 2026 ARS510 radar contributors. The complete MIT permission and warranty notice is retained in THIRD_PARTY_NOTICES.md. Startup/settings ownership is adapted to the current StarPilot architecture; no original firmware binaries or safety permissions are copied.
|
||||
|
||||
## Spotify Bluetooth output
|
||||
|
||||
Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selected speaker through a process-local ALSA configuration, leaving the system ALSA default, soundd and the warning speaker untouched. That approach is adapted from firestar5683's RoadScore Bluetooth output (`roadscore/prototype/bluetooth_output.py`, firestar5683/StarPilot `RoadScore` at `249eb88b3a397db847e4a595d7e23b7e85d329e5`). The Soloist player itself is never included: each user downloads it from Spotify, and the PulseAudio server that carries its audio is vendored unmodified from Ubuntu (see THIRD_PARTY_NOTICES.md).
|
||||
|
||||
## Tesla Pre-AP pedal control
|
||||
|
||||
The Pre-AP configuration, pedal codec/feedback, VirtualDAS feedforward table, grade compensation, torque-zero estimator and vehicle-specific gains preserve firestar5683's intentional Dom work, including `7837a3c1aab`, `aed0b7fdb9e9` and pinned `b220a37f4bd119e402225a7aa16a7cd84878d0af`. Original author attribution: firestar5683 <168790843+firestar5683@users.noreply.github.com>. Startup/calibration ownership, current upstream button/PID interfaces and native command bounds are adapted to the current StarPilot architecture; the old permissive native gateway is not transplanted. Existing source/license notices remain intact.
|
||||
|
||||
@@ -329,6 +329,7 @@ SConscript([
|
||||
'openpilot/selfdrive/ui/SConscript',
|
||||
'openpilot/starpilot/audio/native/SConscript',
|
||||
'openpilot/starpilot/models/runtime_bridge/SConscript',
|
||||
'openpilot/starpilot/flm/SConscript',
|
||||
])
|
||||
|
||||
# Replay is also the isolated on-device viewer engine.
|
||||
|
||||
@@ -112,3 +112,31 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## PulseAudio server for Spotify
|
||||
|
||||
`openpilot/starpilot/soloist/pulseaudio/` contains unmodified AArch64 binaries from Ubuntu 24.04 packages: PulseAudio 16.1 (LGPL-2.1-or-later), libltdl (LGPL-2.1-or-later), libtdb (LGPL-3.0-or-later), libsoxr (LGPL-2.1-or-later), liborc and speexdsp (BSD-style). The complete copyright and license notices are in that directory's `licenses/`, package hashes in `PACKAGES.sha512`, and the corresponding source is available from Ubuntu's archive for the exact versions listed in its README. The libraries are dynamically linked and may be replaced with `update.sh`.
|
||||
|
||||
## Galaxy Android companion tunnel
|
||||
|
||||
`openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64` is the upstream
|
||||
[hev-socks5-tunnel 2.18.0](https://github.com/heiher/hev-socks5-tunnel/releases/tag/2.18.0)
|
||||
static Linux ARM64 release, under the MIT license. Its notice is retained in
|
||||
`openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel`; the adjacent SHA-256
|
||||
file pins the downloaded artifact. The Android companion uses the Kotlin
|
||||
standard library (Apache-2.0) and the Gradle wrapper (Apache-2.0); no third-party
|
||||
UI framework is bundled.
|
||||
|
||||
## Galaxy Bluetooth companion source snapshot
|
||||
|
||||
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/firestar5683/StarPilot
|
||||
(natehuby76, PRs #224/#225) are included under `openpilot/starpilot/galaxy_bluetooth/`
|
||||
and `openpilot/starpilot/companion/ios/`, with their license files (`UPSTREAM-LICENSE`, `openpilot/starpilot/companion/ios/UPSTREAM-GALAXY-LICENSE`).
|
||||
The bridge runs on openpilot's Python with PyCryptodome 3.23.0 (BSD/public-domain
|
||||
components, from opendbc). Its other dependencies are vendored unmodified, as
|
||||
pure-Python sources, in `openpilot/starpilot/galaxy_bluetooth/vendor/`:
|
||||
dbus-next 0.2.3 (MIT), ifaddr 0.2.0 (MIT) and Zeroconf 0.151.3
|
||||
(LGPL-2.1-or-later). Their license texts are in `vendor/licenses/`, and
|
||||
`vendor/VENDOR.json` records each PyPI source file and its SHA-256. Zeroconf is
|
||||
kept as separate, unmodified source files that can be replaced with another
|
||||
compatible version, as the LGPL requires.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
profile="${1:-c4}"
|
||||
case "${profile}" in
|
||||
c3) big=1 ;;
|
||||
c4) big=0 ;;
|
||||
*) echo "Usage: ./device [c3|c4]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
# Run the real manager with host hardware/cloud disabled. The manager owns the
|
||||
# UI and Galaxy processes, just as it does on a comma device.
|
||||
export STARPILOT_CLOUD_DISABLED=1
|
||||
export BIG="${big}"
|
||||
export BLOCK="${BLOCK:+${BLOCK},}replay_owner,map_snapshot_operations"
|
||||
|
||||
exec "${ROOT_DIR}/dev" device
|
||||
@@ -10,7 +10,12 @@ $Go.import("pfeifer.dev/mapd/cereal/custom");
|
||||
# DO rename the structs
|
||||
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
|
||||
|
||||
struct CustomReserved0 @0x81c2f05a394cf4af {
|
||||
struct StarPilotCarControl @0x81c2f05a394cf4af {
|
||||
# The first three fields are pointer slots mapd does not read.
|
||||
hudControl @0 :AnyPointer;
|
||||
steeringLimitInfo @1 :AnyPointer;
|
||||
navigation @2 :AnyPointer;
|
||||
phoneGps @3 :StarPilotCarState.Gps; # sourceMonoTime is comma receive time minus the phone's fix age
|
||||
}
|
||||
|
||||
struct CustomReserved1 @0xaedffd8f31e7b55d {
|
||||
|
||||
@@ -9,69 +9,137 @@ import (
|
||||
math "math"
|
||||
)
|
||||
|
||||
type CustomReserved0 capnp.Struct
|
||||
type StarPilotCarControl capnp.Struct
|
||||
|
||||
// CustomReserved0_TypeID is the unique identifier for the type CustomReserved0.
|
||||
const CustomReserved0_TypeID = 0x81c2f05a394cf4af
|
||||
// StarPilotCarControl_TypeID is the unique identifier for the type StarPilotCarControl.
|
||||
const StarPilotCarControl_TypeID = 0x81c2f05a394cf4af
|
||||
|
||||
func NewCustomReserved0(s *capnp.Segment) (CustomReserved0, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
|
||||
return CustomReserved0(st), err
|
||||
func NewStarPilotCarControl(s *capnp.Segment) (StarPilotCarControl, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4})
|
||||
return StarPilotCarControl(st), err
|
||||
}
|
||||
|
||||
func NewRootCustomReserved0(s *capnp.Segment) (CustomReserved0, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
|
||||
return CustomReserved0(st), err
|
||||
func NewRootStarPilotCarControl(s *capnp.Segment) (StarPilotCarControl, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4})
|
||||
return StarPilotCarControl(st), err
|
||||
}
|
||||
|
||||
func ReadRootCustomReserved0(msg *capnp.Message) (CustomReserved0, error) {
|
||||
func ReadRootStarPilotCarControl(msg *capnp.Message) (StarPilotCarControl, error) {
|
||||
root, err := msg.Root()
|
||||
return CustomReserved0(root.Struct()), err
|
||||
return StarPilotCarControl(root.Struct()), err
|
||||
}
|
||||
|
||||
func (s CustomReserved0) String() string {
|
||||
func (s StarPilotCarControl) String() string {
|
||||
str, _ := text.Marshal(0x81c2f05a394cf4af, capnp.Struct(s))
|
||||
return str
|
||||
}
|
||||
|
||||
func (s CustomReserved0) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
|
||||
func (s StarPilotCarControl) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
|
||||
return capnp.Struct(s).EncodeAsPtr(seg)
|
||||
}
|
||||
|
||||
func (CustomReserved0) DecodeFromPtr(p capnp.Ptr) CustomReserved0 {
|
||||
return CustomReserved0(capnp.Struct{}.DecodeFromPtr(p))
|
||||
func (StarPilotCarControl) DecodeFromPtr(p capnp.Ptr) StarPilotCarControl {
|
||||
return StarPilotCarControl(capnp.Struct{}.DecodeFromPtr(p))
|
||||
}
|
||||
|
||||
func (s CustomReserved0) ToPtr() capnp.Ptr {
|
||||
func (s StarPilotCarControl) ToPtr() capnp.Ptr {
|
||||
return capnp.Struct(s).ToPtr()
|
||||
}
|
||||
func (s CustomReserved0) IsValid() bool {
|
||||
func (s StarPilotCarControl) IsValid() bool {
|
||||
return capnp.Struct(s).IsValid()
|
||||
}
|
||||
|
||||
func (s CustomReserved0) Message() *capnp.Message {
|
||||
func (s StarPilotCarControl) Message() *capnp.Message {
|
||||
return capnp.Struct(s).Message()
|
||||
}
|
||||
|
||||
func (s CustomReserved0) Segment() *capnp.Segment {
|
||||
func (s StarPilotCarControl) Segment() *capnp.Segment {
|
||||
return capnp.Struct(s).Segment()
|
||||
}
|
||||
|
||||
// CustomReserved0_List is a list of CustomReserved0.
|
||||
type CustomReserved0_List = capnp.StructList[CustomReserved0]
|
||||
|
||||
// NewCustomReserved0 creates a new list of CustomReserved0.
|
||||
func NewCustomReserved0_List(s *capnp.Segment, sz int32) (CustomReserved0_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz)
|
||||
return capnp.StructList[CustomReserved0](l), err
|
||||
func (s StarPilotCarControl) HudControl() (capnp.Ptr, error) {
|
||||
return capnp.Struct(s).Ptr(0)
|
||||
}
|
||||
|
||||
// CustomReserved0_Future is a wrapper for a CustomReserved0 promised by a client call.
|
||||
type CustomReserved0_Future struct{ *capnp.Future }
|
||||
func (s StarPilotCarControl) HasHudControl() bool {
|
||||
return capnp.Struct(s).HasPtr(0)
|
||||
}
|
||||
|
||||
func (f CustomReserved0_Future) Struct() (CustomReserved0, error) {
|
||||
func (s StarPilotCarControl) SetHudControl(v capnp.Ptr) error {
|
||||
return capnp.Struct(s).SetPtr(0, v)
|
||||
}
|
||||
func (s StarPilotCarControl) SteeringLimitInfo() (capnp.Ptr, error) {
|
||||
return capnp.Struct(s).Ptr(1)
|
||||
}
|
||||
|
||||
func (s StarPilotCarControl) HasSteeringLimitInfo() bool {
|
||||
return capnp.Struct(s).HasPtr(1)
|
||||
}
|
||||
|
||||
func (s StarPilotCarControl) SetSteeringLimitInfo(v capnp.Ptr) error {
|
||||
return capnp.Struct(s).SetPtr(1, v)
|
||||
}
|
||||
func (s StarPilotCarControl) Navigation() (capnp.Ptr, error) {
|
||||
return capnp.Struct(s).Ptr(2)
|
||||
}
|
||||
|
||||
func (s StarPilotCarControl) HasNavigation() bool {
|
||||
return capnp.Struct(s).HasPtr(2)
|
||||
}
|
||||
|
||||
func (s StarPilotCarControl) SetNavigation(v capnp.Ptr) error {
|
||||
return capnp.Struct(s).SetPtr(2, v)
|
||||
}
|
||||
func (s StarPilotCarControl) PhoneGps() (StarPilotCarState_Gps, error) {
|
||||
p, err := capnp.Struct(s).Ptr(3)
|
||||
return StarPilotCarState_Gps(p.Struct()), err
|
||||
}
|
||||
|
||||
func (s StarPilotCarControl) HasPhoneGps() bool {
|
||||
return capnp.Struct(s).HasPtr(3)
|
||||
}
|
||||
|
||||
func (s StarPilotCarControl) SetPhoneGps(v StarPilotCarState_Gps) error {
|
||||
return capnp.Struct(s).SetPtr(3, capnp.Struct(v).ToPtr())
|
||||
}
|
||||
|
||||
// NewPhoneGps sets the phoneGps field to a newly
|
||||
// allocated StarPilotCarState_Gps struct, preferring placement in s's segment.
|
||||
func (s StarPilotCarControl) NewPhoneGps() (StarPilotCarState_Gps, error) {
|
||||
ss, err := NewStarPilotCarState_Gps(capnp.Struct(s).Segment())
|
||||
if err != nil {
|
||||
return StarPilotCarState_Gps{}, err
|
||||
}
|
||||
err = capnp.Struct(s).SetPtr(3, capnp.Struct(ss).ToPtr())
|
||||
return ss, err
|
||||
}
|
||||
|
||||
// StarPilotCarControl_List is a list of StarPilotCarControl.
|
||||
type StarPilotCarControl_List = capnp.StructList[StarPilotCarControl]
|
||||
|
||||
// NewStarPilotCarControl creates a new list of StarPilotCarControl.
|
||||
func NewStarPilotCarControl_List(s *capnp.Segment, sz int32) (StarPilotCarControl_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}, sz)
|
||||
return capnp.StructList[StarPilotCarControl](l), err
|
||||
}
|
||||
|
||||
// StarPilotCarControl_Future is a wrapper for a StarPilotCarControl promised by a client call.
|
||||
type StarPilotCarControl_Future struct{ *capnp.Future }
|
||||
|
||||
func (f StarPilotCarControl_Future) Struct() (StarPilotCarControl, error) {
|
||||
p, err := f.Future.Ptr()
|
||||
return CustomReserved0(p.Struct()), err
|
||||
return StarPilotCarControl(p.Struct()), err
|
||||
}
|
||||
func (p StarPilotCarControl_Future) HudControl() *capnp.Future {
|
||||
return p.Future.Field(0, nil)
|
||||
}
|
||||
func (p StarPilotCarControl_Future) SteeringLimitInfo() *capnp.Future {
|
||||
return p.Future.Field(1, nil)
|
||||
}
|
||||
func (p StarPilotCarControl_Future) Navigation() *capnp.Future {
|
||||
return p.Future.Field(2, nil)
|
||||
}
|
||||
func (p StarPilotCarControl_Future) PhoneGps() StarPilotCarState_Gps_Future {
|
||||
return StarPilotCarState_Gps_Future{Future: p.Future.Field(3, nil)}
|
||||
}
|
||||
|
||||
type CustomReserved1 capnp.Struct
|
||||
@@ -3327,306 +3395,312 @@ func NewMapdOut_GpsSource_List(s *capnp.Segment, sz int32) (MapdOut_GpsSource_Li
|
||||
return capnp.NewEnumList[MapdOut_GpsSource](s, sz)
|
||||
}
|
||||
|
||||
const schema_b526ba661d550a59 = "x\xda\x94z\x0d\x90T\xd5\x95\xff9\xef\xf5L\xcf'" +
|
||||
"M\xf3\x1a\x01\x01\xdb!\x928\xc41\xc0\xc8?\x0eh" +
|
||||
"\x86\xa1g@\xa8\xeea\xde<>\x1c\x94\xaa\xdc\xe9w" +
|
||||
"\xa7\xe7\xc1\x9b\xf7\x9a\xf7^\xcfLSX \x81*\xf4" +
|
||||
"\x8f\x15\xcdj\x82)\xd9\xf8\x81\xb5\x9a\x95\x84du\x8b" +
|
||||
"XX\x0b\x14\xa9B\x17\xaa4\xa5)c\xcc\xfaQ\xd9" +
|
||||
"\xd2d\xb5\x8cY\xad5\xd9X\xbdu\xee\xed\xaf\xf9\x88" +
|
||||
"8E\xd5t\xf7\xef\x9cw\xee\xb9\xe7\x9e{\xee\xf9\xdd" +
|
||||
"\xc7r6\x7fmhE\xf3?\x8d\x81bdkjj" +
|
||||
"\x0b\xd1\xdd\x03\xef:\xc1O\x0e@t!\x16\x06\x1a\xb6" +
|
||||
".\x1ez\xfe\xab\xcfA(\x0c\xd0\xfe\x8b5;P\xbb" +
|
||||
"\xbc&\x0cj\xe1\xa7\x9f$;v\xfc\xe9\xfc\xdd\xd3h" +
|
||||
"=MZ/\x08\xad\xdb\x13\xe7\xbf;\xf8\xd5\x1f\x1d\x86" +
|
||||
"\xe8B\xa5\xa2\x05\xd8\xfe\xf0\x9aM\xa8\x9d\\#\xd5\xdd" +
|
||||
"\x10`\xa1\xed\xc5\xdc\xed?^p\xdf\x03\xa0\xb7a}" +
|
||||
"\xe1\xbc\xf3o\x8d\xdb.\xdc\xf9\xdfP\x83\xa4\xb3t\xed" +
|
||||
"\xbd\xa8\xdd\xba\x96\xbev\xac}K\x01,\xfc\xe3\xd7?" +
|
||||
"\xcaY{w|\x1f\xa2_Q\x0a\x07?\xfd\xf4+\xed" +
|
||||
"\xff\xf5\xf1\x09\xb2\xbc'q\x10\xb5\xc3\x890\x80vw" +
|
||||
"\xe2\x07\x80\x859\xcfb&s\xf1\xf2#\xd3x\xfaJ" +
|
||||
"b\x10\xb5\xffL\x90\xa7m\xbf\x8d\xab\xdd\xe1\xe1\xc7\xa6" +
|
||||
"\xd1:\x97\xd8\x81\xdakB\xcbV\xdb\xd7~6X\xfb" +
|
||||
"\x04\xe8\x0b\xb1J\xadF\xe8\xfd\x8c\xf4~\x99\x90\x8fl" +
|
||||
"G\xc0\x8a_\xfaB\x9cU\xa5^K:\xabz\xaeF" +
|
||||
"\xad\xa7\xe7k\x00\xed\xacg,\x0cX\xe8N\x9c}\xe9" +
|
||||
"\xe4[O<9%Z\x7fH\x1dD\xed\xf3\x14\xcd\xe9" +
|
||||
"/\xa9o\x02\x16\xb6\x9e\xd0\x7fs\xc3\xe8\xe5'\xa7\xf1" +
|
||||
"\xf6\xf3\xd4\x0e\xd4\xa2\xbd\xe4\xedO~\x9b~\xe8\xff?" +
|
||||
"\xf5\xd1?O\xb1\xf7aju\xc5\xdef\xc0\xc2\xce\xe7" +
|
||||
"\xde[\xf1\xdd\xcf\xdf\xfa\xc94\xf6\x9a{w\xa0\xd6\"" +
|
||||
"\xecu\xbe\xf5\xc7\xbbR\xd7n?5\xed\xa8\x83\xa5Q" +
|
||||
"/b\xb2\xc5J;\xa7\xa7\xd1\xfa\x90|C\xa15|" +
|
||||
"\xe6\x1f\x1e4\xd6t\xbf0\x8d\xd6\xef\xc8\xd6\xc7)\xd2" +
|
||||
":\xa4\xde\xf46\xbf\xe5\xba\xf3\xd3h]&\xadw\x84" +
|
||||
"\xd6G\xff\xf2\xadWn\xbeu\xd7\xc5\xc9\xab\xa2\x90\xde" +
|
||||
"\x0b\xa99\xa8]\xa6\xb9\xb6\xbf\x98\x8a\xd3\xaa\xac\x1e\xd8" +
|
||||
"\x9e\xb5_\xfb\xd1\xbfO7t/\x0d-\x1c|\xf9\xe0" +
|
||||
"\xa3\x99\xff}\xe3\xfb\x97\xa7\x1b\x9a\xb4\xde\x11Z\x8b/" +
|
||||
"\xa4\xdf|38\xf8\xca\x94\x10\xbf\xd0\xbb\x0e\xb5\xcb\xbd" +
|
||||
"b\xd8^\x91\x0c\x1d\x1dg\x1e\xbbt\xef\xff\xfc\x9a\xbc" +
|
||||
"T'o\xac\xcd\x9bP\xbb\xbc\x99\x16\xe4\xc5\xcd\xef\x01" +
|
||||
"\x16V%\xbb\xbfsl\xfb\xf7\xdf\x98f\xf4\x93};" +
|
||||
"P;\xd7G\xa3w|\xe7\xa5\x1f\xfe\xa0\xe6\x07\xffA" +
|
||||
"&\x95Ij\xc7\xfb\xd6\xa1v\xb2\x8fL>\xdd7\x06" +
|
||||
"X\xb8\xfe\x95_\x7ftC\xcf\xb3\x7f\x9e\xb2c\x9a\xf5" +
|
||||
"]\xa8\xb5\xe8\xa4\xb9X\xcf\x00V6\xdfD\xb3r+" +
|
||||
"n$\xed\x9d\xfa<\x80v\xae\x87)m\x17\x1c?\xbe" +
|
||||
"\xb1\xfe\x83\xab>\x99\xc6\xd7\x91\x81\x1d\xa8\xdd=@\xbe" +
|
||||
">\x11\xca~\xbe\xe6\xd0}\x7f\x99Fk'i\xed\x11" +
|
||||
"Z\xcb\xde_\xda\xfb\xde\x9d\xdf\xfa\xeb\x94x\xa6\x06\x06" +
|
||||
"Qc\x03\xe4\xe4\xce\x81\xfd\x80\x85\xc7\xfd\xbe\x8b/\xed" +
|
||||
"|\xe2\xaf\x93\x9d\x14\x8b~|\xe0 j?\x1b\x10\xe1" +
|
||||
"\x1a\x10\xd1\x8f\xfe\xeb\xd8\x91\x0f\xbb\x06\xff6\xcd\xf0m" +
|
||||
"w\x0c\xa2\xd6u\x07\x0d\xbf\xff\xd8\xa9\xf7\x8ccG\x0a" +
|
||||
"\x933I\xcc|\xf1\x1d\xcf\xa3\xb6\xea\x0era\xc5\x1d" +
|
||||
"?\x85\xc2\xdf\xfd\xf7P!\xcd=\xce\xeco\xa4C9" +
|
||||
"?pG\xbe\x91\x16\x1f7\xa6Y\xd6\xc9\xaeN\x88\x1f" +
|
||||
"\xfd\xdc\xe7\xde(W\xcd\x9b\xfa\x10g\xa2\xbf\xfcJ\xfa" +
|
||||
")\x9657:\xd9\\\xb0%\x9f\xe5\x00}\x88\xfa\x19" +
|
||||
"T\x00\xb46e\x13\x00\xce\xd2Z\x95\x9f\x03`Dk" +
|
||||
"U\x1e\x07\xc0\xd9Z\xab\xf2c\x00\x8cj\xad\xcay\x00" +
|
||||
"\x9c\xa3\xb5*o\x00`\xa3\xd6\xa6\x0c\x02\xa0\xa6\xb5*" +
|
||||
"/\x01`Lk\x13\x9f\xa8\xadP\xf6\x02\xa0\xa2\xb5)" +
|
||||
"\xbb\x00p\xae\xd6*~_\xa5-U\xfe\x08\x80\xf3\xb4" +
|
||||
"V\xe5W\x008_kS\xde\x05\xc0\x05\xda\x0a\xf1\xfb" +
|
||||
"jm\x95\xf2C\x00\\\xa8\xad\x12\xe3.\xd2V\x09{" +
|
||||
"\x8b\xb5\x0e\xf1\xfb\x1a\xadC\xf8\xa5\x16\x7f\x87\xb4\x0e\xe1" +
|
||||
"O\\\xeb\x10v\xaf\xd5n\x15\xf6Z\xda\xbb\x94\xd5\x08" +
|
||||
"\x805\xdaF\xe5y\x00\xac\xd56\x0a\x07\x9a\xb5\x1ee" +
|
||||
"\x07\x006i]\xc2\xb1%\xda\xad\xe2\xc1\xafh]\xc2" +
|
||||
"\xf0u\xc5\xcf\xa5\xed]\xca\x1c2\xd0\xa0mT\xee\x05" +
|
||||
"\xc0\xafj\x1b\x95?\x03\xe0\xd7\xdaS\xca\x12\x12\\\xaf" +
|
||||
"m\x15!hm\x1fP\x14\x02\x96\xb5\xef\x94_\xbe\xae" +
|
||||
"1\xe5{\x00x\x83\xc6\xc4\xa3m\x1a\x13\xc1\x0b\x17\x7f" +
|
||||
"\xd7iL9\x08\x80\xf5\xdaN\xf1y\xa36@>\x14" +
|
||||
"Lw\xcc\xb1]f\x02@\xc1\xe7\xc1\x16\xe6e8\x06" +
|
||||
"I\x16p\x8f\xd9\xf1\xaet\x9a\xdb\x84\x1bY\xceML" +
|
||||
"Z#V\xb0yh(\xec\xf3`\x12\x9ap\x9dH\xe0" +
|
||||
"\xb9B9\xc5\xb2\x89\\\x8d7\xca\x85<\xe1:$\x00" +
|
||||
"\x9f\x07\xdb,\xdfr\x9dDn\x82H\x95\x0f%\xddL" +
|
||||
"\x92CxT\x8e'4\x15\xa9*|\"\x97\xba\xa4\x8f" +
|
||||
"\xd5\xb2\x94\xe5H\xf16\x80\x82\xc7i&\x06\x87\xce " +
|
||||
"\xb0\x9c\x8c_\xf0\xd9(7x\x10@D\xfe\xe4A\x8f" +
|
||||
"\xc3\x06m\xe8\x94\xc3O6\xb6\xd5\xe7B\xce\x8dHI" +
|
||||
",\xa6\xa2L\x90e9G\xb3<{E\xcc\xbeJ\x1a" +
|
||||
".>y\x9bk\x9bI\x85\xf9\x81\xc1\xb9#TI\x13" +
|
||||
"\x83\xaa(\x0bt\x13W\xbd\xdd\x93\xc1\xaet\xb8\x18x" +
|
||||
"\x81*\x12\xddb\x8d\xf0\xcdCC>\x0fd \xba\xf9" +
|
||||
"\x10\xcb\xa1\x1d$\x99\xc3\xb7[a3\x18.\xbb\x8c\xa5" +
|
||||
"\xb8\xc5E\xe0\x0a\x14\x18R\xc7\x9c\x1dPD\xac0\x05" +
|
||||
"\x84\xd0~\x9evkFF\xb8crSH\x9c\x8cO" +
|
||||
"ke\xd8\xeeX\xb7;\xe6\xacw\xbd^>.\x1dH" +
|
||||
"Fh\xb2\x95\xb9o\xcdN\x90Z\xe1\xa2\x94\xe6n\xa8" +
|
||||
"\xa59\x07\xdb\x87-\x9b'\x86\x99\x93\xb1\x9c\x8c\xc1;" +
|
||||
"\xa5\xba\x18\xbd\x8f{>Z~\xc0\x9d\x80\x04r\xd9\xd2" +
|
||||
"\xccIs\xbb\xdb\x85N\x99\x9b\xc5\xf4\xd8\xe4\x83\xea:" +
|
||||
"\xc5\x1f\x86\x0b\x91\x9c\x97\xe6bQ\xc7\x03\xee)\x0e\xb3" +
|
||||
"\xcba\x9e\x90\x8eB\x8c%q<9a\x0e2{\xfb" +
|
||||
"<+\xeezV\x90/\xe3\xaa4CN\xf3~\xbe'" +
|
||||
"gy\xdc\xa7\xdd\x90\xc5\xa0\xc0\xe830\xb2X\x1aM" +
|
||||
".G\x9f\xc7}_\xd9\xc0\xfc-nWQc\xc2x" +
|
||||
"]\xe6\xae\x9c\xafR\xf8\xe5jVkUb'@%" +
|
||||
"\xa8L\x85V\xddUsAy\x88z1\xc4\xe6Q\xee" +
|
||||
"y\x96\xc9+\x8a\xb4j\x09\xd71\xad\xc0r'\x07\xa3" +
|
||||
"\xb4\x03\x8daf\xbac\x09\xe6\x19\x01\x0bPDO@" +
|
||||
"8\x96rMno[)\xa7R\xc46d\xfd\xa4\x9b" +
|
||||
"f\x112H\xf0&\xdfu\xfa\x90\x05\xc3\xebm\x97\x15" +
|
||||
"g-\xb0Z\x16\x0co\xe1\xe3\xc2\x05\x01\xb0`x\x9d" +
|
||||
"[=d\xd1\x14Y\xa2\xf5\x88x\x0e\xb3\xcb\x87E\xcd" +
|
||||
"4\x87\x85\x110\xaf\xcf\xb2\xdd\xa0\xe8,\xbfqC\xd6" +
|
||||
"\x97\x87\xc6Mj\x08 \x84t\xe0\xe2^\x00\xe3NT" +
|
||||
"\xd1\x18F\x05\xa3\x881$\x9c\xe3&\x00\xc3$<K" +
|
||||
"\xb8\xa2\xc4\xc4A3\x82\xfd\x00\x86M\xf88\xe1\xaa\x1a" +
|
||||
"C\x15@\xcb\x09\xfd\x80\xf0\x03\x84\x87\xeab\x18\x02\xd0" +
|
||||
"\xee\xc2\x95\x00\xc68\xe1\x87\x08\xaf\xa9\x8fa\x0d\xf5\xef" +
|
||||
"\xb8\x03\xc08@\xf8Q\xc2k\x1bbX\x0b\xa0\xdd\x83" +
|
||||
"?\x040\x8e\x12~\x8c\xf0pc\x8c\x0eh\xed!\xbc" +
|
||||
"\x17\xc08F\xf8\x09\xc2\xeb\x9abX\x07\xa0=&\xf4" +
|
||||
"O\x10~\x8a\xf0\xfa\xe6\x18\xd6\x03h'\xd1\x030\x9e" +
|
||||
"!\xfc4\xe1\x0d\xe1\x186\x00h\xcf\xe1\xe3\x00\xc6i" +
|
||||
"\xc2/\xa0\x82-\x8dJ\x0c\x1b\x01\xb4s\xb8\x1a\xc08" +
|
||||
"C\xf8ET\x10\x9bb\xd8\x04\xa0\xfd\x12\x97\x01\x18g" +
|
||||
"\x09~\x13\x15,\xf8.m\x99\x94\x0b\x9d\x8eKi\x85" +
|
||||
"\xf5\xa0`=`\xc1f\x81\x15\xe4L\x0et\xbc\x82\x82" +
|
||||
"\x8d\x84\xb9N\x86@@^\xc6\x98=E/\xee\x8bm" +
|
||||
"\xd4\x00\x0a6\x00\x16\x069\xf3,'\xd3\x0d*\xcf\x94" +
|
||||
"\xc1a\xd7\xb3\xf6\xbaN\x80\xcc\xeeJ\xa7s\x1eS\xd3" +
|
||||
"\xf9\xb2p\x94{\x81\x95f6JQ:O\x07\xe0D" +
|
||||
"k]X\x14uW[\xf5e\xa5LC\\\xc8\xcax" +
|
||||
"\xce\xb1\xc6\xc5\xec\xfc\x80\x8ddS\x96\x1d\xb6-\x1fk" +
|
||||
"@\xc1\x1a\xc0\xcea\xe6\xaf\xb7\xc6\x11AA\x04\x8c\x8c" +
|
||||
"\xf6\xf6t\xe3,\xc0>\x15\x85\x81Y\x80_\x98\x96\xd4" +
|
||||
"\xc3l\xce\x057\x1al$ksJ\xcc\x9c/\x92r" +
|
||||
">%X\xb4k\x1d5\"\xd1\x8e\x95\xd4\x87DW\xd0" +
|
||||
"\xa9\xafF\xdb\x08\x0cE\x97R/P\x13m\xd9\x05\xb0" +
|
||||
"?\xe7\xecv\xdc1'\xee\xb8\x1b\xb2~\xc1q\x13\xee" +
|
||||
"(\xf7\x18\xa8\x19\xbe\xdfqS,H\x0f\x17F\xe8/" +
|
||||
"7{\xa1\xd3\x95%\xa4\x08$AV\xe1/\xdd\x99\x85" +
|
||||
"\xcd\x15+g\xd8\xcau|\x99V\x8e\xf61\x9d\x1a\x9b" +
|
||||
"\xd5\\ \"P\xda\x96\xd1\x87\xef\x05\xd0\x1fQQ\x7f" +
|
||||
"\xaa\xb2'\xa3On\x02\xd0O\xa8\xa8\x9f\xaal\xc8\xe8" +
|
||||
"\xc9e\x00\xfaS*\xea\x17*\xbb1z\x8e4\xcf\xaa" +
|
||||
"\xa8_R\x10Cb'F_<\x08\xa0_TQ\x7f" +
|
||||
"\x95\xb6!\x8am\x18}e\x10@\x7fYE\xfdM\x05" +
|
||||
"\xcb\x9d\x0c\xf6yn\x86\xea$\xb5\x90\x95f\x1c\x10g" +
|
||||
"S\xd2\x94\x8e8\xea\xc7@\xc1&\xc0H\x96\x05\xc3\xa5" +
|
||||
"\x1c\x98]aC\x80\"\x1b\xb2\xae/**\x08{e" +
|
||||
"bS\xb4g\xbbn\xb6\x9f\x05\x1c\xbbh\x013\xbc\x92" +
|
||||
"\xbb%\x09\x84S\x96SFKAU\xff^n\xa9\xb9" +
|
||||
"@\xaf\xc3\xeak\x84\xfa]U\x0c\xa9\xbe\xbfPJ=" +
|
||||
"\x88P\xf2\x156d}\x83\xf65 \xd7\x8f\x94+\xe3" +
|
||||
"\xd3\xea:\xaa,*U\x16\xb5\xaa2\x9eT\xa9R<" +
|
||||
"E\xf8\xb3jUe\xfc\x99J\x15\xf0\x14\xe1gT\x05" +
|
||||
"\xb1X\x18\x7f\xa1R\xa1;M\xf0\x05R\x0f\xa1,\x8c" +
|
||||
"\xe7T*\xbcg\x09\xbfDx\x8d\"\x0b\xe3\x8b\xea\xf3" +
|
||||
"\x00\xc6%\xc2_'\xbcV\x95\x85\xf151\xec\xcb\x84" +
|
||||
"\xbfIx8$\x0b\xe3o\x84\xfd\xd7\x09\xff=\xe1u" +
|
||||
"\xaa,\x8c\xef\xa8T\x18\x7fO\xf8\x9f\x08\xaf\x0f\xc9\xc2" +
|
||||
"\xf8\xa1J\x85\xf1\x03\xc2?#\xbc\xa1F\x16\xc6O\xd5" +
|
||||
"\xef\x01\x18\x9f\x11\x1e\x0a)\x18m\xac\x95\x85\x11C\xbf" +
|
||||
"\x020\xeaB*\x1a1\xc2\x9b\xde\x96\x951\x1a\"\x7f" +
|
||||
"\x9a\x08\x9fOx\xf3\xe2\x186\x03hsCT\xf0g" +
|
||||
"\x13\xbe\x88\xf0Y\xef\xc4p\x16\x80\xb6 D~\xce'" +
|
||||
"\xfc:\xc2#u1\x8c\x00h-\xa1\x97\x00\x8c\xeb\x09" +
|
||||
"\xbf\x89\xf0\xd9\xf51\x9cM4,D\xf1YN\xf8-" +
|
||||
"\x84G\x1bb\x18\x05\xd0:B4\xaf[\x08\xbf\x8d\xf0" +
|
||||
"9\x91\x18\xce\x01\xd0zB\x83\x00F7\xe1}\x84k" +
|
||||
"\x8d1\xd4\x00\xb4T\xe8\xe7\x00F\x1f\xe1w\x12\x1ek" +
|
||||
"\x8aa\x0c@\x1b\x08\xd1\x81r'\xe1\xc3\x84\xcfm\x8e" +
|
||||
"\xe1\\:\xf8B\x14\x1f\x93\xf0,\xe1W-\x8a\xe1U" +
|
||||
"t\xf0\x09\xfd,\xe1\xfb\x08\x9f\xf7n\x0c\xe7\x01hy" +
|
||||
"\xe1\xcf>\xc2\x8f\x10>\x7fq\x0c\xe7\x03h\x87C\xbb" +
|
||||
"\x00\x8cC\x84\xdfO\xf8\x82\xba\x18.\x00\xd0\xee\x13\xf1" +
|
||||
"9B\xf8\x83\x84_]\x13\xc3\xab\x01\xb4\x07\x84\x9f\x0f" +
|
||||
"\x12\xfe(\xe1\x0b\xaf\x89\xe1B\x00\xed\xb8\xf0\xe7\x11\xc2" +
|
||||
"\x9f\"|Q<\x86\x8b\x00\xb4'E<O\x10~\x8a" +
|
||||
"\xf0\xc5\xd7\xc5p1\xa5g\x88\x0e\xe8g\x08?M\xf8" +
|
||||
"5\x0d1\xbc\x86\x0e>\x11\x9fg\x09?Kx\xbc1" +
|
||||
"\x86q\x00\xed\x051\xaf\xb3\x84_\"\xfc\xda\xa6\x18^" +
|
||||
"Ky(\xf0K\x84\xbfNxKs\x0c[(\x0fC" +
|
||||
"\x07\x01\x8cW\x09\x7f;\xa4\xe0\xfe1\x96\xefe#\xbc" +
|
||||
"T\x0a:\xc7X\xbe\x9f\x0f\x95~\x16<\x97\x99$\xaf" +
|
||||
"\xaa\x16\xf2\xd8IZ#\xa0ZAy[;\xc5&\x18" +
|
||||
":e\xcb5E\x80\x12\xef\xb6:\xfd\x80\xda\xdb\x92B" +
|
||||
"\xe70\xdb\xcb<\xb3l\x9d\xf4oc{\x19\xa8\xd3\x80" +
|
||||
"\xe8\x99\xdd\x16=\xafV\x0c\x14\x989j\xf9\xae\x97\x87" +
|
||||
"\xb81\xe1$\xa6\x87\xba\xccQ\x0bI(]\x98\"S" +
|
||||
"J\xb2\xa2\xdd4V\x1cs\x1d\xbe\x9d\xe5K\x07e\xdc" +
|
||||
"f\x0e\xf7\xb1\x16\x14\xac\x05,\x04\x96\xcd\x93D\x1bU" +
|
||||
"n\x96T\xca\x91Q\xac\xc0\xc8e2\xdc\x0f\xb8)\x8c" +
|
||||
"W\x9d\xe8~Q\x00\x9d\xe6Dw\xb9\x1fX#TH" +
|
||||
"\xcd~\x97\x99\xdb-S\x0d\x86\xcbBZ\x07\xeab!" +
|
||||
"\xcc\xc7\x03\x8cT\xee\x0f\x011\x02X0\xadbT\xd7" +
|
||||
"{\xee\xc8v\x96O\xc4\xb9CM\x7f\xb9\xc7(\x12O" +
|
||||
",1\xcf*\x8fF\x88/y\xa3|r\xfc\xc6X\xde" +
|
||||
"\xe06O#\x15\x7fyc\x81\x91\xca5Pq\xe4\xd2" +
|
||||
"\x9c\xd1\x92\x9d{P\x1d\x90a+3<\xc6\xf2\x09\x88" +
|
||||
"\xd8\xcc\xf71R\xb9\x94\x93O\xc7\xc7X~\xa3Yj" +
|
||||
"M\x0a\xe9b\xf3>\x89\xabT2O\x14\xfem\x1c\xe2" +
|
||||
"\x1eM\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd1 \xe5" +
|
||||
"\x93\xa3\xe8b\xa6r@`\xa4r\x94T\xa4)\xd7q" +
|
||||
"\xb7@\xb8\xba!L\xbb#\xd9\\\xc0M\x142\x8bv" +
|
||||
"@Y&\xbb\xc8\x0d\x1c\x1d\xee1y2\x96eY\xcf" +
|
||||
"5si\xee\xa1\xc1}\xf2\xb1\"\xf9\xc2^\xbfLU" +
|
||||
"$\xbd\x15\xd1\xa6\x96\xa2N4\x09\xd1\xd5\xa2\xa9\xaa_" +
|
||||
"\x07@\xbb'\xb0\xd2\xfb\xb3\xdcKs'\x98IO\xb3" +
|
||||
"\xaa\xba\xa7\x99\xee\xf8\xa5\xbcKt\xbaN\xc0\xc7E?" +
|
||||
"\xd3$\x06_,;\xba\xb9\xcbDG\xd7\xbc\x0e`\xff" +
|
||||
"\x90\xc7\xf9\x18\xcbG\xd2V\x90/ur3\xf1d\xc5" +
|
||||
"\x8c\xba\xb1\xb0\xb9b\xa67q\xdf\x9c\xe9\x00\xabf\xfa" +
|
||||
"\xc0\x8a+\x05S\xdc\xf5\xa1Cq\x8c\x95\xfb\xc2\xbb\xa8" +
|
||||
"\xdb\x1bWQ?T\xd5\x17\xde\xbd\x12@\xdf\xa7\xa2~" +
|
||||
"DA,\xb6\x85\x87\x97\x00\xe8\x07T\xd4\x8fR[8" +
|
||||
"[\xb6\x85\xf7\xd0\xd3\x87T\xd4\xef\xaf4\"\xd1\xfb\xa8" +
|
||||
"W<\xaa\xa2~L\xc1H\x90\xcfR\x8a\x97\xdf\xed\x14" +
|
||||
"w\xd9\x10\xb1\xd5\xd2\xbe\x0e\xfb\x81W\xee\xfc\x06]\xd7" +
|
||||
".\xef\xd6]E\xe2Z]\xecg\x12\x93\xe53\x0db" +
|
||||
"\xfb\x95\x1e\xb8\xadX?\xa8z\x88\xfdp\xb3\x88\xcec" +
|
||||
"2%\x1f\xde$R\xf2\xa1]\x82d<\xb0R\x90\x8c" +
|
||||
"{\xfa\x05\xc98L*\xb5\xd1\xbb\x06\x010\x1c\xcd\x13" +
|
||||
"X\x17\xcdy\xb4S\xa3{\xe8\xb9\x86\xe8\x08=\xd7\x18" +
|
||||
"\xb5\xe8\xa3)\xcaI\xb39\xca\xaa\xc8Ia\xc4\x0d\\" +
|
||||
"o\x8c\xe5\x01\xa0\xf2=\x92\xb4\x9c\xdd\xf1\xc0\xcb9\xbb" +
|
||||
"\x0b\xe2o\xd2r\x00w\xef\xcfz\xd6\x08\xf3\xf2\x85\xe2" +
|
||||
"g\x12\xc2\x96\xb3\xbb\xe0s*j\xcc\x03\xccW\xbe\xc7" +
|
||||
"\xf3d\xa3\x10\x10\xf1c\x9e0_\xfe.\xcc\x17rN" +
|
||||
"\x9a\xe6lAd\xc8\xe2f\xc1\xe3\xbeer*\xff\x16" +
|
||||
"\xb3\x0b\xb65j9\x19\xea\x7f=\xce\x83+\xd2\x14Z" +
|
||||
"\xd2>\xd7r\x02yy0\xbb\x9c\x8d\x8cR\xe7\xdb*" +
|
||||
"\xeavU6Z\xfd\x00\xfa\xb0\x8az@\xcdqH\xa6" +
|
||||
"\xe3\x1e\x02\xb3*\xea\xfb(\x1dkd:\xe6\xf7V\x92" +
|
||||
"\xf9K\x13\xe8t\xce\x1beA\xce\x83\xcaA[\x08\xe4" +
|
||||
"\x05&\x87N\xdb\xa5\x922\x85+|\x89\x1d\xdf\xfee" +
|
||||
"\x08[_\x89\xc9\x88\xd2Z\x8eC+\xc5\xe1z\x15\xf5" +
|
||||
"\x9b\xaa\xe2\xb0\x82\xa6\xbc\\E\xfd\x96\x99\xcc\xee\x0a\x0e" +
|
||||
"\x10q\x16tE\\\xdd\x89\xd5\x10\xf1m]&\x12\xba" +
|
||||
"E&\xf4\xe2M\"\xa1\x17,\x01\x888\xae\xc3\x0b\x16" +
|
||||
"\x9d\xe5\x0e\xb3)Q\xf8x\xf9{8\xcd\xbc/\x1cr" +
|
||||
"\xc2\x15R$`\x01\xd7CX\xf5>7\x8aK\xc2\x1b" +
|
||||
"\xb2\xbe~\xa0\xcc\x9a\xf6\xa8\xd4\xf6f\x89F\xec\xabf" +
|
||||
"MyAk\xf6\x11~\xa4\x9a5\x1dV\xa9-=D" +
|
||||
"\xf8\xfd\x84\xab(i\xd3}\xea\xe3\x00\xc6\xfd\x84?\"" +
|
||||
"h\x93*i\xd3\xc3\xc2\xfe1\xc2O\x08\xda\x14\x92\xb4" +
|
||||
"\xe91\x95\xda\xd2G\x09\x7fF\xd0\xa6\x1aI\x9b\x9e\x16" +
|
||||
"v\x9e!\xfc\xb4\xa0M\xb5\x926=\xa7\x9e\x9f@\xcb" +
|
||||
"\xea\xc2\x926\x9d\x13\xac\xef\x0c\xe1\x17\x05m\xaa\x93\xb4" +
|
||||
"\xe9\x97\x82f] \xfceA\x9b\xea%m\xba,X" +
|
||||
"_\x85\xae56H\xda\xf4\x9a\xf0\xf3U\xc2\xdf&\xbc" +
|
||||
"\xa9Q\xd2\xa6\xdf\x09\x9a\xf56\xe1\x1f\x10\xde\xdc$i" +
|
||||
"\xd3\x1fTj\xdb\xdf'\xfc\x13\xc2g5K\xda\xf4\xb1" +
|
||||
"\xa0q\x9f\xa8*\xf6\x0b\xd64K\xb2\xa6\xcf\xc5\xb0\x7f" +
|
||||
"#\xf5:\xc1\x9a\"\x925\xd5\x88.?D\xdd\xf9l" +
|
||||
"\xc1\x9afK\xd6\xd4,XD\x85\xad\xcd\x89J\xd64" +
|
||||
"7\xf4\xfc\x04V\xa6\xa9\x925\xb5\x84\xc8\xcd\xeb\x08_" +
|
||||
".X\xd3\x1c\xc9\x9a\xda\x84\x9d\x1b\x08\xbfY\xb0&M" +
|
||||
"\xb2\xa6UB\xfff\xc2\xbb\x05k\x8aI\xd6\xd4%X" +
|
||||
"M\x85\x95\xcd\x9b+YSJ\xd8I\x12~\xbb`M" +
|
||||
"WI\xd6\xb4U\xb0\x8b-\x84\x7f[\xb0\xa6y\x925" +
|
||||
"\xed\x14l\xb0\xc2\xd6\xae^+Y\x13\x17\xe3\x0e\x13\x1e" +
|
||||
"\x08\xd6\xd4%Y\xd3\x1ea\xa7\xc2\xd6\x16\xad\x93\xac)" +
|
||||
"/\xf0q\xc2\x0f\x09\xd6\x94\x90\xac\xe9n\x81\x1f \xfc" +
|
||||
"\xa8`M\xdd\x925\xdd#\xfc,\xb35\x8cK\xd2\xf4" +
|
||||
"@h\x09\x80q\x94\xe0c!\x05\xc5}\xb6\xdd\xe7q" +
|
||||
"\x88\xf8~U\xf7\xca\xec1\x96\xf77;X|\x1b\xd4" +
|
||||
"\xd3)^qT\x14\x06=\xb6\x9b'\xad\x0c\x84\x87\x03" +
|
||||
"\xbf\x8c\x9a\xcc\x1f\x1et\x99\x87f\xd5\xcdv\xb9\xa6\x99" +
|
||||
"|\xda\xb1\xca\x0d\xbc\xb8\xe1\xe6&L\x95$]'C" +
|
||||
"\xd2\xf0t\xcf)\xdb\xb8\x97/iT?\xbf\x9f\xa7\xdd" +
|
||||
"\x0d\x9cy\xe5'\x86\\/\xcd\x13.\x03\xd5\x0f\xca\xa0" +
|
||||
"\xe5\xf71o7\xd1\x82\xca\xc0Y\x96\xf3y\x92\x05\x10" +
|
||||
"\xa1\xc9O\x82]\x07E\xf5\xb3\x1c\x86v\x15\x01r\xbd" +
|
||||
"`\x03g\x80\x95\xf1\x02\x8f\x0d\x0dY\xe9\x14\xba\xa6|" +
|
||||
"GTM\x102\xcc7\x02\x96\xde]=\xee\x88kr" +
|
||||
"\x9a\x05L\x98\xa8,l}\x1e\xc4\xf9\x17.RW\xa7" +
|
||||
"m\xbbc\xd5\x11\xaa,G\xe0f\x0d+\x83N\x99\xc5" +
|
||||
"\xc9\x17-S\x8dJ<\xe9b)\x9e8Y\xb6\x8dc" +
|
||||
")\xde\x93\x9e\xcdr\x93\xd9)6\x0e\xf1~\x9e\xe1\xce" +
|
||||
"D\\\xc4\xad+\x1dX\xa3\xbc2\xe1\x80\xfb6K$" +
|
||||
"z\xa0\xd3\xc9\xb0L\x95\xa9\xa2\xa0\x1b-\x9f\x0b\x11N" +
|
||||
"\x91\xf5\xa2\x1bty#\xd5\x09#\xb2\xf96\x9ap\xc2" +
|
||||
"\xcbY~\xd5@\"\xf7\xa6\x95ds\xb6\xcf\xbb\x1c\x13" +
|
||||
"\xe2\x1bl\xcb\xe4%<\x9c\xc9\xfa8\xbbr`\x14\xef" +
|
||||
"\xfcfp.\xdf<\xc3\xce\xfd\xff]I\x7f{\x91\x95" +
|
||||
"\x0aR\x1a\xceg\xc5)*\xbb\x94U\xb2-l\xeb\x17" +
|
||||
"\xa7h\xab<E\x97n\x12ma\xcb2\x80\xfd\xe9\x9c" +
|
||||
"\xe7\x11i\xcaz\xdc\xb4\xd2\x01\x074\x0bY\xd7\xf7\xad" +
|
||||
"A\x9b\x97\xceV\xc7\x14\xbb 2\xc4,\xfb\x8a\x17\xe1" +
|
||||
"\xdd\xc5+\xd7\xf2\x8d\xeb\xa4k\xe0\xd5\x00\xfa\x83*\xea" +
|
||||
"\x8fV5\x16\xc7\xfb\xab\xee\x86\x15\x94\xae?\xb9\xa3\xea" +
|
||||
"nXUd\x83u\xf2 \x80\xfe\x8c\x8a\xfa\xe9\xca5" +
|
||||
"\xf0s\xf4\xf4\xb3*\xea/W]\x03_&\xc5K*" +
|
||||
"\xea\xef+\xd8\xc9DvMJW\x9b\xe6Z\xc9\x1d7" +
|
||||
"`\xf6z\xcb\x06\x95\xfbX\x07\x0a\xd6A\xd5\xf517" +
|
||||
"\xd7[6\xf7\xa1,\xb1\x8b\xef\xad\x00\xfd\xd2-qS" +
|
||||
"\xf1MAI\x84\xdd<`\x96\xedC\xe5\x1a\xb9\xfc\x1f" +
|
||||
"6\x8a\xd7\xc83!\x06\x13\xb2\xe0J\xc1/\xbdV\xeb" +
|
||||
"\xee\x94>\x08\xeaZ^\x83\x1ej\xee\xbaU\xd4\xfb\x14" +
|
||||
",-A\x8a\xa2\x9dTQ\xbf\xbdj\x09\xb6R\x10\xb7" +
|
||||
"\xa8\xa8\x7f[\xa9\x9ar\x15\x15\x9aY\xd4\xfe/\x00\x00" +
|
||||
"\xff\xff\x00\xd1\x99X"
|
||||
const schema_b526ba661d550a59 = "x\xda\x94Z}lT\xd7\x95?\xe7\xbd\xb1\xc7\x9f\x8c" +
|
||||
"\x877\x04\x0c&\x13(\xd0\x98\xc6i\x8c\xf16\xa6I" +
|
||||
"]c\x03\xc1\x1a\x83\xc7\xc3G@A\xea\xf5\xbc\xeb\x99" +
|
||||
"\x07\xcf\xefM\xde{c{\x10\x11\x84\x82\x04YP\x93" +
|
||||
"n\xd2\x92*lC\x02R\xda\x0d-\xed\x86\x15\x1bQ" +
|
||||
"-AD\"Y\x90\x92\xaa\xa9\x9a4\xbbI\xa3JI" +
|
||||
"\xba\x89\xd2v\x13m\xb6\xdb\xe8\xad\xce\xbd\xf3\xf1\xfc\xd1" +
|
||||
"\x10\xff\xc3\xf8\xfd\xcey\xe7\x9e{\xee\xb9\xe7\x9e\xdf}" +
|
||||
"\xdc\xf1\xf5\xfao\x86\xda\x1b3\x0d\xa0$\x1f\xae\xaa\xf6" +
|
||||
"\xa3{v\xbccy?9\x00\xd1E\xe8\xef\xa8\xdb\xba" +
|
||||
"x\xe4\xf9\x15\xe7!\x14\x06\xe8h\xae\xde\x89Z[u" +
|
||||
"\x18T\xff\xa7\x1f'\xbav\xfe\xf1\xf2\x83\x93\xb5\xaa\x84" +
|
||||
"Zm\xf5^\xd4\x16W\x87\x01\xb4\xe6\xeaw\x01\xfd{" +
|
||||
"{/\x7fgx\xc5\x0f\x0fCt\x91RQ\x06\xec\xc0" +
|
||||
"p?j\xf3\xc2\xf4R4l\x87\x00\xfd\xb6\x97\xf2\xf7" +
|
||||
"\xfe\xb8\xf9\xf8#\x90l\xc3Z\xff\xb2\xf5o\xf5\xdb\xae" +
|
||||
"\xdc\xf7\xdfP\x85\xa4s\xbc\xfe!\xd4\xce\xd4\xd3\x9f\xa7" +
|
||||
"\xea\xdfR\x00\xfd\x7f\xfc\xcaGyc\xef\xce\xefA\xf4" +
|
||||
"K\x8a\x7f\xf0\x93O\xbe\xd4\xf1_\x7f:M\x96_\x9f" +
|
||||
"s\x10\xb5\x0f\xe7\x90\x0f\xef\xcf\xf9>\xa0?\xf79\xcc" +
|
||||
"d\xae^\x7fb\x86iuE\x86Q\x1b\x88\xd0\xb4\xda" +
|
||||
"~\x1bW\xfb\xc2\xd9S3h\xb5Fv\xa2v\xb7\xd0" +
|
||||
"2\xd5\x8eo~:\\\xfd4$\x17\xe1\xb4\xd97\x93" +
|
||||
"^[D\xbe\xb2\x1d\x01+~%\x17\xe1\x9c\x80:E" +
|
||||
"\xa8\xe3d\xd3B\xd4\xce6}\x19\xa0\xe3\xa5\xa6\xf10" +
|
||||
"\xa0\xdf\xd7{\xe9\xe5\xb3o=}fZ\xb4\x92\xcd\x07" +
|
||||
"Q\xe3\xcd4'\xd6\xfc5@\x7f\xeb\xe9\xe4\xeb\xb7\x8d" +
|
||||
"]?3\x83\xb7\xbcy'j\x85f\xf2\xf6'\xbfM" +
|
||||
"?\xf6\xf7\xcf|\xf4O\xd3\xecmm^S\xb1\xb7\x19" +
|
||||
"\xd0\xdfu\xfe\xdd\xf6\xef|\xf6\xd6Of\xb0\x97'{" +
|
||||
"G\x85\xbd\xee\xb7\xfe\xf0\xc0\xc0-\xdb\xcf\xcd8\xeap" +
|
||||
"i\xd4\xab\x98Xb\xa4\xad\x0b3hm%[\x86\xd0" +
|
||||
"\xca^\xfc\x87GS_\xef\xfb\xc5\x0cZ\xeb\xc8\xd6\x0e" +
|
||||
"\xa1uH]\xfd6\xbfk\xd9\xe5\x19\xb4:Ik\xa3" +
|
||||
"\xd0\xfa\xe8\x9f\xbf\xf1\xea\x9dw\xef\xbe:uU\x14\xd2" +
|
||||
"[\xde<\x17\xb5N\x9akG{s\x9cVe\xcd\x8e" +
|
||||
"\xed9\xf3\xb5\x1f\xfe\xfbLC/\xa4\xa1\x17\x92\xd1W" +
|
||||
"\x0e>\x99\xf9\xbf7\xbew}\xa6\xa1Ik\xa3\xd0Z" +
|
||||
"|%\xfd\xe6\x9b\xde\xc1W\xa7\x85x\xf9\xc2\xb5\xa8u" +
|
||||
".\x14\xc3.\x14\xc9\xd0\xd5u\xf1\xd4\xb5\x87\xfe\xe7\xd7" +
|
||||
"\xe4\xa5:\xc5\xe2\x92E\xfd\xa8u.\xa2\x05i_D" +
|
||||
"\x1b\xa73\xd1\xf7\xed\x13\xdb\xbf\xf7\xc6\x0c\xa3\xcfk\xd9" +
|
||||
"\x89Zk\x0b\x8d\xde\xf5\xed\x97\x7f\xf0\xfd\xaa\xef\xff'" +
|
||||
"\x99T\xa6\xa8U\xb5\xacEm^\x0b\x99\x8c\xb6\x8c\x03" +
|
||||
"\xfa\xb7\xbe\xfa\xeb\x8fn[\xf7\xdc\x9f\xa7\xed\x98|\xcb" +
|
||||
"n\xd4\x8e\x0a\xcd\xc3-\x19\xc0\xca\xe6\x9blVn\xc5" +
|
||||
"\x9f\x91\xf6\x8b-\xf3\x01:\xae\xb7\x84)m\x9bO\x9e" +
|
||||
"\xdcX\xfb\xc1M\x1f\xcf\xe0\xebk\xcbv\xa2\xf6\xfe2" +
|
||||
"\xf2\xf5\xe9P\xee\xb3\xaf\x1f:\xfe\xbf3h\xbdHZ" +
|
||||
"\xaf\x0b\xad\x95\xef-\xdf\xf4\xee}\xdf\xf8\xcb\xb4x\x9e" +
|
||||
"_6\x8c\xdaK\xcb\xc8\xc9\x17\x97\xed\x07\xf4\x9fr\x07" +
|
||||
"\xaf\xbe\xbc\xeb\xe9\xbfLuR,z\xd5\xf2\x83\xa85" +
|
||||
"/\x17\xe1Z.\xa2\x1f\xfd\x97\xf1#\x1f\xf6\x0c\xffu" +
|
||||
"\x86\xe1\x1f[1\x8c\xda\x8fV\xd0\xf0\xfbO\x9c{7" +
|
||||
"u\xe2\x88?5\x93\xc4\xcc\x0f\xafx\x1e\xb5\x93+\xc8" +
|
||||
"\x85\xc7W\xfc\x14\xda\xfc4w83\xbf\x9a\x0e\xe5]" +
|
||||
"\xcf\x1e\xfdjZ\xfc\xdc\x9ef9+\xb7\xa6W<\x0c" +
|
||||
"q\x97;c\\\xd5W\x0f\"~\xae~\xcac\xce\xa0" +
|
||||
"a\xda^/sz\xbbm\xcbsls\x101\xd9\xa4" +
|
||||
"\x86\x00B\x08\x10e;\x01\x92\xdfR1i*\x18E" +
|
||||
"\x8c!\x81\xc6w\x01\x92\xa6\x8a\xc9\x09\x05\xa3\x8a\x12C" +
|
||||
"\x05 \x9a'MO\xc5\xe4\x01\x05\xa3\xaa\x1aC\x15 " +
|
||||
"\xfa@?@r\x9f\x8a\xc9#\x0a\xfa\xd9\xbc\xdeKc" +
|
||||
"\x80j\x9b8\x17\x14\x9c\x0b\xe8\xbb\x1e\xe7\x8eae0" +
|
||||
"a\x8c\x1a\xdeFk\x04\xed\xb2\xccbcF\x86y\x06" +
|
||||
"\xa8\xb6U\x06sY\xdb\xe2\x1br.\x00`S\xa5\x80" +
|
||||
"\x03b\x13|\xfel\x07XN\xdfh\xe5\xf2\xde\x96B" +
|
||||
"\x8e\x03\xd0D/\x92\xebZ\x9b\xd2\x0f\x80s\xb4V\xe5" +
|
||||
"\xe7\x00\x18\xd1Z\x95\xa7\xc8\xb8\xd6\xaa\xfc\x18\x00\xa3Z" +
|
||||
"\xabr\x19\x00\xe7j\xad\xca\x1b\x00X\xaf\xb5)\xc3\x00" +
|
||||
"\xa8i\xad\xca\xcb\x00\x18\xd3\xda\xc4/j\xed\xca^\x00" +
|
||||
"T\xb46e7\x00\xce\xd3Z\xc5\xf3M\xdar\xe5\x0f" +
|
||||
"\x008_kU~\x09\x80\x0b\xb46\xe5\x1d\x00l\xd6" +
|
||||
"\xda\xc5\xf3B\xadS\xf9\x01\x00.\xd2:\xc5\xb8-Z" +
|
||||
"\xa7\xb0\xb7X\xeb\x12\xcf7k]\xc2/\xb5\xf8\x1c\xd2" +
|
||||
"\xba\x84?q\xadK\xd8\xbdE\xbb[\xd8[\xd2\xd1\xa3" +
|
||||
"\xacA\x00\xac\xd26*\xcf\x03`\xb5\xb6Q8\xd0\xa8" +
|
||||
"\xadSv\x02`\x83\xd6#\x1c[\xaa\xdd-^\xfc\x92" +
|
||||
"\xd6#\x0c/+\xfe.\xef\xe8Q\xe6\x92\x81:m\xa3" +
|
||||
"\xf2\x10\x00\xae\xd06*\x7f\x06\xc0/w\x0c(KI" +
|
||||
"p\xab\xb6U\x84\xa0\xb5c\x87\xa2\x10\xb0\xb2c\x97\xfc" +
|
||||
"\xe3+\x1aS\xbe\x0b\x80\xb7iL\xbc\xda\xa61\x11\xbc" +
|
||||
"p\xf1\xb9Fc\xcaA\x00\xac\xd5v\x89\xdf\xdb\xb5\x1d" +
|
||||
"\xe4\x83\xaf\xdb\xe3\x96i3\x1d\x00|\x97{[\x98\x93" +
|
||||
"\xe1\xe8%\x98\xc7\x1df\xc6{\xd2in\x12\x9e\xcaq" +
|
||||
"\xae\xcb\x14\xd9<2\x12v\xb97\x05\xed\xb5\xad\x08e" +
|
||||
"/\xc1\x03,\xd7\x9b\xafr\xc6\xb8\x90\xf7\xca\xb4\x06\x97" +
|
||||
"{\xdb\x0c\xd7\xb0\xad\xde\xfc$\x91*_J\xd8\x99\x04" +
|
||||
"\x87\xf0\x98\x1cOh*RU\xf8D.\xf5H\x1f\x83" +
|
||||
"\xb2\x01\xc3\x92\xe2m\x00\xbe\xc3i&)\x0e\xdd\x9eg" +
|
||||
"X\x19\xd7w\xd9\x18Oq\xcf\x83\x88|\xe4\xde:\x8b" +
|
||||
"\x0d\x9b\xd0-\x87\x9fjl\xab\xcb\x85\x9c\xa7\"%\xb1" +
|
||||
"\x98\x8a2I\x96\xe3\x1c\xf5\xf2\xec\x151\xfb\x804\\" +
|
||||
"|\xf3\x1e\xdb\xd4\x13\x0as\xbd\x14\xe7\x96P%M\xf4" +
|
||||
"\x02Q\x16h?W\x9d=S\xc1\x9et\xb8\x18x\x81" +
|
||||
"*\x12\xddb\x8c\xf2\xcd##.\xf7d \xfa\xf8\x08" +
|
||||
"\xcb\xa3\xe9%\x98\xc5\xb7\x1ba\xdd\xcb\x96]\xc6R\xdc" +
|
||||
"\xe2\"p>\x05\x86\xd41oz\x14\x11#L\x01!" +
|
||||
"t\x88\xa7\xed\xaa\xd1Qn\xe9\\\x17\x12+\xe3\xd2Z" +
|
||||
"\xa5L{\xbc\xcf\x1e\xb7\xd6\xdb\xce&>!\x1dHD" +
|
||||
"h\xb2\x95\xb9o\xcdM\x92\x1a\xe1\xa2\x94\xe6\x9eRK" +
|
||||
"s\xf6\xb6g\x0d\x93\xf7f\x99\x951\xacL\x8awK" +
|
||||
"u1\xfa w\\4\\\x8f[\x1e\x09\xe4\xb2\xa5\x99" +
|
||||
"\x95\xe6f\x9f\x0d\xdd27\x8b\xe9\xd1\xefR\x1d*>" +
|
||||
"\xa4l\x88\xe4\x9d4\x17\x8b:\xe1qG\xb1\x98Y\x0e" +
|
||||
"\xf3\xa4t\x14b,\x89\xe3\x89Is\x90\xd9;\xe8\x18" +
|
||||
"q\xdb1\xbcB\x19W\xa5\x19r\x9a\x0f\xf1\xfb\xf3\x86" +
|
||||
"\xc3]\xda\x0d9\xf4|F\xbf^*\x87\xa5\xd1\xe4r" +
|
||||
"\x0c:\xdcu\x95\x0d\xcc\xddb\xf7\x145&\x8d\xd7\xa3" +
|
||||
"\xef\xce\xbb*\x85_\xaefP\xab\x12;\x01*^e" +
|
||||
"*\xb4\xea\xb6\x9a\xf7\xcaC\xd4\x8a!6\x8fq\xc71" +
|
||||
"t^Q\xa4U\xeb\xb5-\xdd\xf0\x0c{j0J;" +
|
||||
"0\x95e\xba=\xde\xcb\x9c\x94\xc7<\x14\xd1\x13\x10\x8e" +
|
||||
"\x0f\xd8:7\xb7\xad\x92S)b\x1brn\xc2N\xb3" +
|
||||
"\x08\x19$\xb8\xdf\xb5\xadAd^v\xbdi\xb3\xe2\xac" +
|
||||
"\x05V\xcd\xbc\xec\x16>!\\\x10\x00\xf3\xb2k\xed\xe0" +
|
||||
"\x90ESd\x89\xd6#\xe2X\xcc,\x1f\x16U78" +
|
||||
"\x1a\xc9Y~\xbb8s\xe8\xd0X]:\x1d\xb5]\xb8" +
|
||||
"\x17 u\x1f\xaa\x98\xcab\xe5\x80\xd48\xf6\x03\xa4t" +
|
||||
"\xc2sX9#\xb5Q\x1c\x02H\x99\x84O`\xe5\x98" +
|
||||
"\xd4\xf2B\xdf#\xfc\x00\xe1\xa1\x9a\x18\x86\x00\xb4\x07p" +
|
||||
"\x15@j\x82\xf0C\x84W\xd5\xc6\xb0\x0a@{\x10w" +
|
||||
"\x02\xa4\x0e\x10~\x8c\xf0\xea\xba\x18V\x03hG\xf1\x07" +
|
||||
"\x00\xa9c\x84\x9f <\\\x1f\xa3&B{\x0c\x1f\x02" +
|
||||
"H\x9d \xfc4\xe15\x0d1\xac\x01\xd0N\x09\xfd\xd3" +
|
||||
"\x84\x9f#\xbc\xb61\x86\xb5\x00\xdaYt\x00R\xcf\x12" +
|
||||
"~\x81\xf0\xbap\x0c\xeb\x00\xb4\xf3\xf8\x14@\xea\x02\xe1" +
|
||||
"WP\xc1%\xf5J\x0c\xeb\x01\xb4\x17p\x0d@\xea\"" +
|
||||
"\xe1WQAl\x88a\x03\xf5L\xb8\x12 u\x89\xe0" +
|
||||
"7QA\xdf\xb5i\xcb\x0c\xd8\xd0m\xd9\x94VX\x0b" +
|
||||
"\x0a\xd6\x02\xfa&\xf3\x0c/\xafs:\xd3\xebA\xc1z" +
|
||||
"\xc2l+C /c\xcc\x9c\xa6\x17w\xc56\xaa" +
|
||||
"\x03\x05\xeb\x00\xfda\xce\xa8\x9b\xe8\x03\x95g\xca`\xd6" +
|
||||
"v\x8c\xbd\xb6\xe5!3{\xd2\xe9\xbc\xc3\xd4t\xa1," +
|
||||
"\x1c\xe3\x8eg\xa4\x99\x89R\x94.\xd0\x018\xd9Z\x0f" +
|
||||
"\x16E}A\xab\xae\xac\x94i\x88\x0bY\x19\xcf[\xc6" +
|
||||
"\x84\x98\x9d\xeb\xb1\xd1\xdc\x80a\x86M\xc3\xc5*P\xb0" +
|
||||
"\x0a\xb0;\xcb\xdc\xf5\xc6\x04\"(\x88\x80\x91\xb1M\xeb" +
|
||||
"\xfap\x0e\xe0\xa0\x8a\xc2\xc0\x9c@\x0fS\xf57z\x98" +
|
||||
"\xcdy\xef\xf6\x14\x1b\xcd\x99\x9c\x123\xef\x8a\xa4\\ " +
|
||||
"\x9a\xb0\x9e\xb5\xd4\x88D\xbbVQ\x1f\x12m\xa7S_" +
|
||||
"\x8d\xb6\x11\x18\x8a.\xa7^\xa0*\xbad7\xc0\xfe\xbc" +
|
||||
"\xb5\xc7\xb2\xc7\xad\xb8eo\xc8\xb9\xbee\xf7\xdac\xdc" +
|
||||
"a\xa0f\xf8~\xcb\x1e`^:\xeb\x8f\xd2\xbf\\\xdf" +
|
||||
"\x04\xdd\xb6,!E \x01\xb2\x0a\x7f\xe1>4\xac\xb7" +
|
||||
"\xaf\xbaQ#:\xa5q\xed\xba\x91>\x85\x81\xf61\x9d" +
|
||||
"\x1a\x9b\xd5\xbc'\"PnZ\x1f\x7f\x08 \xf9\x84\x8a" +
|
||||
"\xc9g\x02M\xeb\x19jEO\xab\x98<\x17hZ\xcf" +
|
||||
"\xae\x04H>\xa3b\xf2J\xa0i}\x814/\xa9\x98" +
|
||||
"\xbc\xa6 \x86\xc4N\x8c\xbet\x10 yU\xc5\xe4\xaf" +
|
||||
"h\x1b\xa2\xd8\x86\xd1W\x87\x01\x92\xaf\xa8\x98|S\xc1" +
|
||||
"r'\x83\x83\x8e\x9d\xa1:)\xfa\xd32a(\xf6\xa7" +
|
||||
"n\xe9\x88\xa3~\x0c\x14l\x00\x8c\xe4\x98\x97-\xe5@" +
|
||||
"S\x85\xb1\x01\x8al\xc8\xd9\xae\xa8\xa8\xb2\xdf-\x93\xaf" +
|
||||
"\xa2=\xd3\xb6sC\xcc\xe3\xd8C\x0b\x98\xe1\x95\xdc-" +
|
||||
"I <`Xe\xb4\x14T\xf5o\xe5\x96\x9a\xf7\x92" +
|
||||
"5\x18\xbc\xea\xa8\xdd\x1d`q\xb5C~)\xf5 B" +
|
||||
"\xc9\xe7o\xc8\xb9)\xda\xd7\x80<y\xa4\\\x19\x7f\xa4" +
|
||||
"\xae\xa5\xca\xa2ReQ\x03\x95\xf1\xacJ\x95\xe2\x19\xc2" +
|
||||
"\x9fS\x03\x95\xf1g*U\xc0s\x84_T\x15\xc4b" +
|
||||
"a\xfcW\x95\x0a\xdd\x05\x82\xaf\x90z\x08ea|A" +
|
||||
"\xa5\xc2{\x89\xf0k\x84W)\xb20\xbe\xa4>\x0f\x90" +
|
||||
"\xbaF\xf8o\x08\xafVea|M\x0c\xfb\x0a\xe1o" +
|
||||
"\x12\x1e\x0e\xc9\xc2\xf8\xba\xb0\xff\x1b\xc2\x7fOx\x8d*" +
|
||||
"\x0b\xe3\xefT*\x8c\xbf'\xfc\x8f\x84\xd7\x86da\xfc" +
|
||||
"P\xa5\xc2\xf8\x01\xe1\x9f\x12^W%\x0b\xe3'\xeaw" +
|
||||
"\x01R\x9f\x12\x1e\x0a)\x18\xad\xaf\x96\x85\x11C\xbf\x04" +
|
||||
"H\xd5\x84TL\xc5\x08ox[V\xc6h\x88\xfci" +
|
||||
" |\x01\xe1\x8d\x8bc\xd8\x08\xa0\xcd\x0bQ\xc1o\"" +
|
||||
"\xbc\x85\xf09\xbf\x8b\xe1\x1c\x00\xad9D~. |" +
|
||||
"\x19\xe1\x91\x9a\x18F\x00\xb4%\xa1\x97\x01R\xb7\x12\xbe" +
|
||||
"\x9a\xf0\xa6\xda\x186\x11\x9f\x0fQ|\xee \xfc.\xc2" +
|
||||
"\xa3u1\x8c\x02h]!\x9a\xd7]\x84\xdfC\xf8\xdc" +
|
||||
"H\x0c\xe7\x02h\xebB\xc3\x00\xa9>\xc2\x07\x09\xd7\xea" +
|
||||
"c\xa8\x01h\x03\xa1\x9f\x03\xa4\x06\x09\xbf\x8f\xf0XC" +
|
||||
"\x0cc\x00\xda\x8e\x10\x1d(\xf7\x11\x9e%|^c\x0c" +
|
||||
"\xe7\xd1\xc1\x17\xa2\xf8\xe8\x84\xe7\x08\xbf\xa9%\x867\xd1" +
|
||||
"\xc1'\xf4s\x84\xef#|\xfe;1\x9c\x0f\xa0\x15\x84" +
|
||||
"?\xfb\x08?B\xf8\x82\xc51\\\x00\xa0\x1d\x0e\xed\x06" +
|
||||
"H\x1d\"\xfca\xc2\x9bkb\xd8\x0c\xa0\x1d\x17\xf19" +
|
||||
"B\xf8\xa3\x84/\xac\x8a\xe1B\x00\xed\x11\xe1\xe7\xa3\x84" +
|
||||
"?I\xf8\xa2\x9bc\xb8\x08@;)\xfcy\x82\xf0g" +
|
||||
"\x08o\x89\xc7\xb0\x05@;#\xe2y\x9a\xf0s\x84/" +
|
||||
"^\x16\xc3\xc5\x94\x9e!:\xa0\x9f%\xfc\x02\xe17\xd7" +
|
||||
"\xc5\xf0f:\xf8D|\x9e#\xfc\x12\xe1\xf1\xfa\x18\xc6" +
|
||||
"\x01\xb4_\x88y]\"\xfc\x1a\xe1\xb74\xc4\xf0\x16\xca" +
|
||||
"C\x81_#\xfc7\x84/i\x8c\xe1\x12\xca\xc3\xd0A" +
|
||||
"\x80\xd4\xaf\x08\x7f;\xa4\xe0\xfeqV\xd8\xc4Fy\xa9" +
|
||||
"\x14t\x8f\xb3\xc2\x10\x1f)=\xfa\x8e\xcdt\x92\x07\xaa" +
|
||||
"\x85<v\x12\xc6(\xa8\x86W\xde\xd6V\xb1\x09\x86n" +
|
||||
"\xd9rM\x13\xa0\xc4\xfb\x8cn\xd7\xa3\xf6\xb6\xa4\xd0\x9d" +
|
||||
"e{\x99\xa3\x97\xad\x93\xfe=l/\x03u\x06\x10\x1d" +
|
||||
"\xbd\xcf\xa0\xf7\xd5\x8a\x01\x9f\xe9c\x86k;\x05\x88\xa7" +
|
||||
"&\x9d\xc4\xf4R\x8f>f \x09\xa5\x0b\xd3dJI" +
|
||||
"V\xb4\x9b\xc6\x8ac\xb6\xc5\xb7\xb3B\xe9\xa0\x8c\x9b\xcc" +
|
||||
"\xe2.V\x83\x82\xd5\x80\xbeg\x98<A\xb4Q\xe5z" +
|
||||
"I\xa5\x1c\x19\xc5\xf0R\xf9L\x86\xbb\x1e\xd7\x85\xf1\xc0" +
|
||||
"\x89\xee\x16\x05\xd0\xadOv\x97\xbb\x9e1J\x85T\x1f" +
|
||||
"\xb2\x99\xbe\xdd\xd0U/[\x16\xd2:P\x17\x0ba>" +
|
||||
"\xe1a\xa4r\xc7\x09\x88\x11@_7\x8aQ]\xef\xd8" +
|
||||
"\xa3\xdbY\xa17\xce-j\xfa\xcb=F\x91xb\x89" +
|
||||
"y\x06<\x1a%\xbe\xe4\x8c\xf1\xa9\xf1\x1bg\x85\x147" +
|
||||
"y\x1a\xa9\xf8\xcb\x1b\x0b\x8cT\xae\xaa\x8a#\x97\xe6\x8c" +
|
||||
"\x86\xec\xdc\xbd`@\xb2F&;\xce\x0a\xbd\x101\x99" +
|
||||
"\xebb\xa4rq(\xdf\x8e\x8f\xb3\xc2F\xbd\xd4\x9a\xf8" +
|
||||
"\xe9b\xf3>\x85\xabT2O\x14\xfem\x1c\xe2\x0eM" +
|
||||
"\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd3 \xe5\x93\xa3" +
|
||||
"\xe8b\xa6r@`\xa4r\x94T\xa4\x03\xb6eo\x81" +
|
||||
"p\xb0!L\xdb\xa3\xb9\xbc\xc7u\x142\x83v@Y" +
|
||||
"&\xbb\xc8\x0d\x1c-\xee0y2\x96e9\xc7\xd6\xf3" +
|
||||
"i\xee`\x8a\xbb\xe4cE\xf2\xb9\xbd~\x99\xaaHz" +
|
||||
"+\xa2M-E\x8dh\x12\xa2kDSU\xbb\x16\x80" +
|
||||
"v\x8fg\xa4\xf7\xe7\xb8\x93\xe6\x967\x9b\x9e\xa63\xd8" +
|
||||
"\xd3\xcct\xfcR\xde\x89K8>!\xfa\x99\x061\xf8" +
|
||||
"b\xd9\xd1\xcd[):\xba\xc6\xb5\x00\xfbG\x1c\xce\xc7" +
|
||||
"Y!\x926\xbcB\xa9\x93\x9b\x8d'\xed\xb3\xea\xc6\xc2" +
|
||||
"z\xfb\xeaY\xb6o_\x9b\xed\x00\x9d\xb3}\xa1\xfdF" +
|
||||
"\xc1\x14w}hQ\x1cc\xe5\xbe\xf0\x01\xea\xf6&T" +
|
||||
"L\x1e\x0a\xf4\x85\x0f\xae\xaa\\Qb\xb1-<\xbc\x14" +
|
||||
" y@\xc5\xe41j\x0b\x9bd[x\x94\xde>\xa4" +
|
||||
"b\xf2\xe1J#\x12=N\xbd\xe21\x15\x93'\x14\x8c" +
|
||||
"x\x85\x1c\xa5x\xf9\xfbSq\x97\x8d\x10[-\xed\xeb" +
|
||||
"\xb0\xeb9\xe5\xceo\xd8\xb6\xcd\xf2n\xdd]$\xae\xc1" +
|
||||
"b?\x9b\x98\xdc1\xdb v\xdc\xe8\x85{\x8a\xf5\x83" +
|
||||
"\xaa\x87\xd8\x0fw\x8a\xe8\x9c\x92)\xf9x\xbfH\xc9\xc7" +
|
||||
"v\x0b\x92\xf1\xc8*A2\x8e\x0e\x09\x92q\x98T\xaa" +
|
||||
"\xa3\x0f\x0c\x03`8Z \xb0&\x9awh\xa7F\xef" +
|
||||
"\xa7\xf7\xea\xa2\xa3\xf4^}\xd4\xa0\x9f\x86('\xcd\xc6" +
|
||||
"(\x0b\x90\x13\x7f\xd4\xf6lg\x9c\x15\x00\xa0\xf2w$" +
|
||||
"aX{\xe2\x9e\x93\xb7\xf6\xf8\xe2\xdf\x84a\x01\xee\xd9" +
|
||||
"\x9fs\x8cQ\xe6\x14\xfc\xe2o\x02\xc2\x86\xb5\xc7w9" +
|
||||
"\x155\xe6\x00\x16*\x7f\xc7\x0bd\xc3\xf7\x88\xf81G" +
|
||||
"\x98/\xff-\xcc\xfby+Ms6 2bp\xdd" +
|
||||
"w\xb8k\xe8\x9c\xca\xbf\xc1L\xdf4\xc6\x0c+C\xfd" +
|
||||
"\xaf\xc3\xb9wC\x9aBK:h\x1b\x96'/\x0f\x02" +
|
||||
"W\xeb\xfd3]\xad\x0f\x01$\xb3*&=j\x8eC" +
|
||||
"2\x1d\xef'0\xa7br\x1f\xa5c\x95L\xc7\xc2\xde" +
|
||||
"J2\x7fa\x02\x9d\xce;c\xcc\xcb;P9h}" +
|
||||
"O^`r\xe86m*)\xd3\xb8\xc2\x17\xd8\xf1\x1d" +
|
||||
"_\x84\xb0\x0d\x96\x98\x8c(\xad\xe58\xb4R\x1cnU" +
|
||||
"1\xb9:\x10\x87v\x9a\xf2\x1d*&\xef\x9a\xcd\xecn" +
|
||||
"\xe0\x00\x11gAW\xc4\xd5\x9dX\x0d\x11\xdf\xd6\x95\"" +
|
||||
"\xa1\x97\xc8\x84^\xdc/\x12\xbay)@\xc4\xb2-\xee" +
|
||||
"\x1bt\x96[\xcc\xa4D\xe1\x13\xe5\xbf\xc3i\xe6|\xe1" +
|
||||
"\xaf+\xa9\x88\xc7<\x9e\x0ca\xe0\x9bs\x14\x97\x867" +
|
||||
"\xe4\xdc\xe4\x812k\xba_\xa5\xb67G4b_\x90" +
|
||||
"5\x15\x04\xad\xd9G\xf8\x91 k:\xacR[z\x88" +
|
||||
"\xf0\x87\x09WQ\xd2\xa6\xe3\xeaS\x00\xa9\x87\x09\x7fB" +
|
||||
"\xd0&U\xd2\xa6\xc7\x85\xfd\x13\x84\x9f\x16\xb4)$i" +
|
||||
"\xd3)\x95\xda\xd2'\x09\x7fV\xd0\xa6*I\x9b~$" +
|
||||
"\xec<K\xf8\x05A\x9b\xaa%m:\xaf^\x9eD\xcb" +
|
||||
"j\xc2\x926\xbd X\xdfE\xc2\xaf\x0a\xdaT#i" +
|
||||
"\xd3\x8b\x82f]!\xfc\x15A\x9bj%m\xba.X" +
|
||||
"_\x85\xae\xd5\xd7I\xda\xf4\x9a\xf0\xf3W\x84\xbfMx" +
|
||||
"C\xbd\xa4M\xff!h\xd6\xdb\x84\x7f@xc\x83\xa4" +
|
||||
"M\xef\xab\xd4\xb6\xbfG\xf8\xc7\x84\xcfi\x94\xb4\xe9O" +
|
||||
"\x82\xc6}\xac\xaa8$X\xd3\x1c\xc9\x9a>\x13\xc3\xfe" +
|
||||
"\x95\xd4k\x04k\x8aH\xd6T%\xba\xfc\x10u\xe7M" +
|
||||
"\x8255I\xd6\xd4(XD\x85\xad\xcd\x8dJ\xd64" +
|
||||
"/\xf4\xfc$V\xa6\xa9\x925-\x09\x91\x9b\xcb\x08\xbf" +
|
||||
"C\xb0\xa6\xb9\x925\xb5\x09;\xb7\x11~\xa7`M\x9a" +
|
||||
"dM\x9dB\xffN\xc2\xfb\x04k\x8aI\xd6\xd4#X" +
|
||||
"M\x85\x95\xcd\x9f'Y\xd3\x80\xb0\x93 \xfc^\xc1\x9a" +
|
||||
"n\x92\xaci\xab`\x17[\x08\xff\x96`M\xf3%k" +
|
||||
"\xda%\xd8`\x85\xad-\xfc\xa6dM\\\x8c\x9b%\xdc" +
|
||||
"\x13\xac\xa9G\xb2\xa6\xfb\x85\x9d\x0a[kY+YS" +
|
||||
"A\xe0\x13\x84\x1f\x12\xac\xa9W\xb2\xa6\x07\x05~\x80\xf0" +
|
||||
"c\x825\xf5I\xd6tT\xf8Yfk\x18\x97\xa4\xe9" +
|
||||
"\x91\xd0R\x80\xd41\x82O\x84\x14\x14\xf7\xd9\xe6\xa0\xc3" +
|
||||
"!\xe2\xba\x81\xee\x95\x99\xe3\xac\xe0n\xb6\xb0\xf85h" +
|
||||
"]\xb7\xf8\xc4QQ\x18v\xd8\x1e\x9e02\x10\xcez" +
|
||||
"n\x19\xd5\x99\x9b\x1d\xb6\x99\x83z\xe0f\xbb\\\xd3t" +
|
||||
">\xe3X\xe5\x06^\xdcps\x1d\xa6K\x12\xb6\x95!" +
|
||||
"ix\xa6\xf7\x94m\xdc)\x944\x82\xef\xef\xe7i{" +
|
||||
"\x03gN\xf9\x8d\x11\xdbI\xf3^\x9b\x81\xeaze\xd0" +
|
||||
"p\x07\x99\xb3\x87hAe\xe0\x1c\xcb\xbb<\xc1<\x88" +
|
||||
"\xd0\xe4\xa7\xc0\xb6\x85\xa2\xfa\x19\x16C3@\x80l\xc7" +
|
||||
"\xdb\xc0\x19`e<\xcfa##Fz\x00m]~" +
|
||||
"#\x0a\x12\x84\x0csS\x1eK\xef\x09\x8e;j\xeb\x9c" +
|
||||
"f\x01\x93&*\x0b\xdb\xa0\x03q\xfe\xb9\x8b\xd4\xd3m" +
|
||||
"\x9a\xf6x0B\x95\xe5\xf0\xec\\\xca\xc8\xa0Ufq" +
|
||||
"\xf2C\xcbt\xa3\x12O\xd8X\x8a'N\x95m\xe3X" +
|
||||
"\x8a\xf7\x94ws\\g\xe6\x00\x9b\x80\xf8\x10\xcfpk" +
|
||||
"2.\xe2\xd6\x93\xf6\x8c1^\x99\xb0\xc7]\x93\xf5\xf6" +
|
||||
"\xae\x83n+\xc32\x01SEA\x1f\x1a.\x17\"\x9c" +
|
||||
"&\xdb\x84\xb6\xd7\xe3\x8c\x06\x13Fd\xf3=4\xe1^" +
|
||||
"'o\xb8\x81\x81D\xee\xcd(\xc9\xe5M\x97\xf7X:" +
|
||||
"\xc47\x98\x86\xceKx8\x93sg\xf9\x8d{\xca\xb9" +
|
||||
"|\xe7,;\xf7\xbf\xbb\x91\xfe\xf6\"+\x15\xa44\\" +
|
||||
"\xc8\x89STv)\x9d\xb2-l\x1b\x12\xa7h\xab<" +
|
||||
"E\x97\xf7\x8b\xb6p\xc9J\x80\xfd\xe9\xbc\xe3\x10i\xca" +
|
||||
"9\\7\xd2\x1e\x07\xd4\xfd\x9c\xed\xba\xc6\xb0\xc9Kg" +
|
||||
"\xab\xa5\x8b]\x10\x19a\x86y\xc3\x8b\xf0\xbe\xe2\x95k" +
|
||||
"\xf9\xc6u\xca5\xf0\x1a\x80\xe4\xa3*&\x9f\x0c4\x16" +
|
||||
"'\x87\x02w\xc3\x0aJ\xd7\xcf\xec\x0c\xdc\x0d\xab\x8al" +
|
||||
"\xb0\xce\x1e\x04H>\xabb\xf2B\xe5\x1a\xf8<\xbd\xfd" +
|
||||
"\x9c\x8a\xc9W\x02\xd7\xc0\xd7I\xf1\x9a\x8a\xc9\xf7\x14\xec" +
|
||||
"f\"\xbb\xa6\xa4\xabIs\xad\xe4\x8e\xed1s\xbda" +
|
||||
"\x82\xca]\xac\x01\x05k p}\xcc\xf5\xf5\x86\xc9]" +
|
||||
"(K\xcc\xe2w+@\xb7tK\xdcP\xfcRP\x12" +
|
||||
"a\x1f\xf7\x98a\xbaP\xb9F.\xff\xa7\x92\xe25\xf2" +
|
||||
"l\x88\xc1\xa4,\xb8Q\xf0K\x9f\xd5\xfa\xba\xa5\x0f\x82" +
|
||||
"\xba\x96\xd7`\x1d5w}*&\x07\x15,-\xc1\x00" +
|
||||
"E;\xa1b\xf2\xde\xc0\x12l\xa5 nQ1\xf9-" +
|
||||
"%0\xe5\x00\x15\x9a]\xd4\xfe?\x00\x00\xff\xff\xd6\xbd" +
|
||||
"\x94\xef"
|
||||
|
||||
func RegisterSchema(reg *schemas.Registry) {
|
||||
reg.Register(&schemas.Schema{
|
||||
|
||||
+113
-13
@@ -1,8 +1,11 @@
|
||||
package cereal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"capnproto.org/go/capnp/v3"
|
||||
"pfeifer.dev/mapd/cereal/custom"
|
||||
@@ -15,6 +18,8 @@ const (
|
||||
externalFixMaxAge = 500_000_000 // 10 Hz source: 500 ms
|
||||
internalFixMaxAge = 2_000_000_000 // 1 Hz source: 2 s
|
||||
carFixMaxAge = 2_500_000_000 // Optional CAN observation: 2.5 s
|
||||
phoneFixMaxAge = 10_000_000_000 // Companion app in the background sends sparsely: 10 s
|
||||
gpsModeRefresh = 1_000_000_000 // Galaxy navigation setting reread interval
|
||||
maxHorizontalAccuracy = 50.0 // input sanity ceiling, not road-match qualification
|
||||
maxClockPairSkew = 1_000_000 // an interrupted pair cannot date a source event
|
||||
maxClockOffsetDrift = 1_000_000 // a larger BOOTTIME/MONOTONIC step fences queued fixes
|
||||
@@ -39,8 +44,53 @@ const (
|
||||
GpsSourceInternal
|
||||
GpsSourceExternal
|
||||
GpsSourceCar
|
||||
GpsSourcePhone
|
||||
)
|
||||
|
||||
// Galaxy navigation's GPS source setting: backup uses the phone only without
|
||||
// comma or car GPS, comma never uses it, phone prefers it whenever it has a fix.
|
||||
const (
|
||||
GpsModeBackup = "backup"
|
||||
GpsModeComma = "comma"
|
||||
GpsModePhone = "phone"
|
||||
)
|
||||
|
||||
var storagePrefix = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
|
||||
|
||||
func navigationSettingsPath() string {
|
||||
prefix := os.Getenv("OPENPILOT_PREFIX")
|
||||
if prefix == "" {
|
||||
return "/data/starpilot/navigation/settings.json"
|
||||
}
|
||||
if !storagePrefix.MatchString(prefix) {
|
||||
return ""
|
||||
}
|
||||
return "/data/starpilot-" + prefix + "/navigation/settings.json"
|
||||
}
|
||||
|
||||
// ReadGpsMode returns the saved choice, or backup when it is missing or invalid.
|
||||
func ReadGpsMode() string {
|
||||
path := navigationSettingsPath()
|
||||
if path == "" {
|
||||
return GpsModeBackup
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil || len(raw) > 1<<20 {
|
||||
return GpsModeBackup
|
||||
}
|
||||
var settings struct {
|
||||
GpsSource string `json:"gpsSource"`
|
||||
}
|
||||
if json.Unmarshal(raw, &settings) != nil {
|
||||
return GpsModeBackup
|
||||
}
|
||||
switch settings.GpsSource {
|
||||
case GpsModeComma, GpsModePhone:
|
||||
return settings.GpsSource
|
||||
}
|
||||
return GpsModeBackup
|
||||
}
|
||||
|
||||
type GpsSample struct {
|
||||
Location log.GpsLocationData
|
||||
Source GpsSource
|
||||
@@ -60,7 +110,12 @@ type GpsSub struct {
|
||||
gpsLocation Subscriber[log.GpsLocationData]
|
||||
gpsLocationExternal Subscriber[log.GpsLocationData]
|
||||
carGps *Subscriber[custom.StarPilotCarState_Gps]
|
||||
phoneGps *Subscriber[custom.StarPilotCarState_Gps]
|
||||
car gpsCandidate
|
||||
phone gpsCandidate
|
||||
mode func() string
|
||||
modeValue string
|
||||
modeReadAt uint64
|
||||
internal gpsCandidate
|
||||
external gpsCandidate
|
||||
selected GpsSource
|
||||
@@ -83,6 +138,8 @@ func GpsFixFreshAt(source GpsSource, fixMonoTime, now uint64) bool {
|
||||
return now-fixMonoTime <= externalFixMaxAge
|
||||
case GpsSourceCar:
|
||||
return now-fixMonoTime <= carFixMaxAge
|
||||
case GpsSourcePhone:
|
||||
return now-fixMonoTime <= phoneFixMaxAge
|
||||
case GpsSourceInternal:
|
||||
return now-fixMonoTime <= internalFixMaxAge
|
||||
default:
|
||||
@@ -134,6 +191,35 @@ func (s *GpsSub) clearCandidates() {
|
||||
s.internal = gpsCandidate{}
|
||||
s.external = gpsCandidate{}
|
||||
s.car = gpsCandidate{}
|
||||
s.phone = gpsCandidate{}
|
||||
}
|
||||
|
||||
func (s *GpsSub) gpsMode(now uint64) string {
|
||||
if s.modeValue == "" || now < s.modeReadAt || now-s.modeReadAt >= gpsModeRefresh {
|
||||
read := s.mode
|
||||
if read == nil {
|
||||
read = ReadGpsMode
|
||||
}
|
||||
s.modeValue, s.modeReadAt = read(), now
|
||||
}
|
||||
return s.modeValue
|
||||
}
|
||||
|
||||
// updateWrapped admits a StarPilotCarState.Gps-shaped fix dated by its own source time.
|
||||
func (c *gpsCandidate) updateWrapped(event DecodedEvent[custom.StarPilotCarState_Gps], sensor log.GpsLocationData_SensorSource,
|
||||
clock clockSample, barrierMono, maxAge uint64) bool {
|
||||
sourceTime := event.Value.SourceMonoTime()
|
||||
if sourceTime == 0 || sourceTime > event.LogMonoTime || event.LogMonoTime > clock.monoAfter {
|
||||
return false
|
||||
}
|
||||
location, err := carGpsLocation(event.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
location.SetSource(sensor)
|
||||
valid := event.Valid && !math.IsNaN(float64(location.Speed())) && !math.IsInf(float64(location.Speed()), 0) && location.Speed() >= 0 &&
|
||||
!math.IsNaN(location.Altitude()) && !math.IsInf(location.Altitude(), 0)
|
||||
return c.updateWithAge(DecodedEvent[log.GpsLocationData]{Value: location, Valid: valid, LogMonoTime: sourceTime}, clock, barrierMono, maxAge)
|
||||
}
|
||||
|
||||
func (s *GpsSub) clockReady(clock clockSample) bool {
|
||||
@@ -166,7 +252,7 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
|
||||
clock = s.clock()
|
||||
}
|
||||
ready := s.clockReady(clock)
|
||||
internalNew, externalNew, carNew := false, false, false
|
||||
internalNew, externalNew, carNew, phoneNew := false, false, false, false
|
||||
if event, ok := s.gpsLocation.ReadEvent(); ok && ready {
|
||||
internalNew = s.internal.update(event, clock, s.barrierMono)
|
||||
}
|
||||
@@ -176,23 +262,29 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
|
||||
|
||||
if s.carGps != nil {
|
||||
if event, ok := s.carGps.ReadEvent(); ok && ready {
|
||||
sourceTime := event.Value.SourceMonoTime()
|
||||
if sourceTime > 0 && sourceTime <= event.LogMonoTime && event.LogMonoTime <= clock.monoAfter {
|
||||
location, err := carGpsLocation(event.Value)
|
||||
if err == nil {
|
||||
carNew = s.car.updateWithAge(DecodedEvent[log.GpsLocationData]{Value: location, Valid: event.Valid && !math.IsNaN(float64(location.Speed())) && !math.IsInf(float64(location.Speed()), 0) && location.Speed() >= 0 && !math.IsNaN(location.Altitude()) && !math.IsInf(location.Altitude(), 0), LogMonoTime: sourceTime}, clock, s.barrierMono, carFixMaxAge)
|
||||
}
|
||||
}
|
||||
carNew = s.car.updateWrapped(event, log.GpsLocationData_SensorSource_car, clock, s.barrierMono, carFixMaxAge)
|
||||
}
|
||||
}
|
||||
if s.phoneGps != nil {
|
||||
if event, ok := s.phoneGps.ReadEvent(); ok && ready {
|
||||
// The enum has no phone value; android is the closest.
|
||||
phoneNew = s.phone.updateWrapped(event, log.GpsLocationData_SensorSource_android, clock, s.barrierMono, phoneFixMaxAge)
|
||||
}
|
||||
}
|
||||
|
||||
mode := s.gpsMode(clock.boot)
|
||||
phoneFresh := ready && mode != GpsModeComma && GpsFixFreshAt(GpsSourcePhone, s.phone.fixMonoTime, clock.boot)
|
||||
source := GpsSourceNone
|
||||
if ready && GpsFixFreshAt(GpsSourceExternal, s.external.fixMonoTime, clock.boot) {
|
||||
if phoneFresh && mode == GpsModePhone {
|
||||
source = GpsSourcePhone
|
||||
} else if ready && GpsFixFreshAt(GpsSourceExternal, s.external.fixMonoTime, clock.boot) {
|
||||
source = GpsSourceExternal
|
||||
} else if ready && GpsFixFreshAt(GpsSourceInternal, s.internal.fixMonoTime, clock.boot) {
|
||||
source = GpsSourceInternal
|
||||
} else if ready && GpsFixFreshAt(GpsSourceCar, s.car.fixMonoTime, clock.boot) {
|
||||
source = GpsSourceCar
|
||||
} else if phoneFresh {
|
||||
source = GpsSourcePhone
|
||||
}
|
||||
sample.SourceChanged = source != s.selected
|
||||
if sample.SourceChanged {
|
||||
@@ -215,6 +307,10 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
|
||||
sample.Location = s.car.location
|
||||
sample.FixMonoTime = s.car.fixMonoTime
|
||||
sample.NewFix = carNew
|
||||
case GpsSourcePhone:
|
||||
sample.Location = s.phone.location
|
||||
sample.FixMonoTime = s.phone.fixMonoTime
|
||||
sample.NewFix = phoneNew
|
||||
default:
|
||||
return sample, false
|
||||
}
|
||||
@@ -234,24 +330,28 @@ func (s *GpsSub) Close() {
|
||||
if s.carGps != nil {
|
||||
s.carGps.Sub.Msgq.Close()
|
||||
}
|
||||
if s.phoneGps != nil {
|
||||
s.phoneGps.Sub.Msgq.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func GetGpsSub() (gpsSub GpsSub) {
|
||||
return GpsSub{
|
||||
carGps: optionalCarGpsSubscriber(),
|
||||
carGps: optionalWrappedGpsSubscriber("starpilotCarState", CarGpsReader),
|
||||
phoneGps: optionalWrappedGpsSubscriber("starpilotNavigation", PhoneGpsReader),
|
||||
gpsLocation: NewSubscriber("gpsLocation", GpsLocationReader, true, ms.Settings.SubscriberSettings.ShadowGpsLocation),
|
||||
gpsLocationExternal: NewSubscriber("gpsLocationExternal", GpsLocationExternalReader, true, ms.Settings.SubscriberSettings.ShadowGpsLocationExternal),
|
||||
}
|
||||
}
|
||||
|
||||
func optionalCarGpsSubscriber() (subscriber *Subscriber[custom.StarPilotCarState_Gps]) {
|
||||
func optionalWrappedGpsSubscriber(service string, reader func(log.Event) (custom.StarPilotCarState_Gps, error)) (subscriber *Subscriber[custom.StarPilotCarState_Gps]) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
subscriber = nil
|
||||
slog.Warn("Optional car GPS subscriber unavailable")
|
||||
slog.Warn("Optional GPS subscriber unavailable", "service", service)
|
||||
}
|
||||
}()
|
||||
value := NewSubscriber("starpilotCarState", CarGpsReader, true, false)
|
||||
value := NewSubscriber(service, reader, true, false)
|
||||
return &value
|
||||
}
|
||||
|
||||
|
||||
@@ -2623,7 +2623,7 @@ struct Event {
|
||||
# DO change the name of the field and struct
|
||||
# DON'T change the ID (e.g. @107)
|
||||
# DON'T change which struct it points to
|
||||
customReserved0 @107 :Custom.CustomReserved0;
|
||||
starpilotNavigation @107 :Custom.StarPilotCarControl;
|
||||
customReserved1 @108 :Custom.CustomReserved1;
|
||||
starpilotCarState @109 :Custom.StarPilotCarState;
|
||||
customReserved3 @110 :Custom.CustomReserved3;
|
||||
|
||||
+2863
-2862
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
package cereal
|
||||
|
||||
import (
|
||||
"capnproto.org/go/capnp/v3"
|
||||
"pfeifer.dev/mapd/cereal/custom"
|
||||
"pfeifer.dev/mapd/cereal/log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func phoneGpsWire(t *testing.T, source, envelope uint64) []byte {
|
||||
t.Helper()
|
||||
message, segment, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event, err := log.NewRootEvent(segment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event.SetLogMonoTime(envelope)
|
||||
event.SetValid(true)
|
||||
navigation, err := event.NewStarpilotNavigation()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gps, err := navigation.NewPhoneGps()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gps.SetSourceMonoTime(source)
|
||||
gps.SetHasFix(true)
|
||||
gps.SetLatitude(41)
|
||||
gps.SetLongitude(-111)
|
||||
gps.SetAltitude(100)
|
||||
gps.SetSpeed(12)
|
||||
gps.SetBearingDeg(90)
|
||||
gps.SetHorizontalAccuracy(5)
|
||||
gps.SetVerticalAccuracy(5)
|
||||
gps.SetBearingAccuracyDeg(10)
|
||||
gps.SetSpeedAccuracy(1)
|
||||
raw, err := message.Marshal()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func attachPhone(s *GpsSub, mode *string) *[][]byte {
|
||||
queue := [][]byte{}
|
||||
s.phoneGps = &Subscriber[custom.StarPilotCarState_Gps]{reader: PhoneGpsReader, maxBytes: 250 * 1024, readBytes: func() []byte {
|
||||
if len(queue) == 0 {
|
||||
return nil
|
||||
}
|
||||
value := queue[0]
|
||||
queue = queue[1:]
|
||||
return value
|
||||
}}
|
||||
s.mode = func() string { return *mode }
|
||||
return &queue
|
||||
}
|
||||
|
||||
func TestPhoneGpsFollowsTheGalaxySetting(t *testing.T) {
|
||||
now := uint64(10_000_000_000)
|
||||
mode := GpsModeBackup
|
||||
s, internal, _ := testGpsSub(&now)
|
||||
car := attachCar(s)
|
||||
phone := attachPhone(s, &mode)
|
||||
|
||||
*phone = append(*phone, phoneGpsWire(t, now-300_000_000, now))
|
||||
sample, ok := s.ReadSample()
|
||||
if !ok || sample.Source != GpsSourcePhone || sample.FixMonoTime != now-300_000_000 || sample.Location.Longitude() != -111 {
|
||||
t.Fatalf("backup without comma or car GPS: %+v %v", sample, ok)
|
||||
}
|
||||
*car = append(*car, carGpsWire(t, now-1, now, true, true))
|
||||
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceCar {
|
||||
t.Fatal("car ranks above the phone in backup")
|
||||
}
|
||||
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
|
||||
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
|
||||
t.Fatal("comma ranks above the phone in backup")
|
||||
}
|
||||
|
||||
mode = GpsModePhone
|
||||
now += gpsModeRefresh
|
||||
*phone = append(*phone, phoneGpsWire(t, now-100_000_000, now))
|
||||
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
|
||||
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourcePhone {
|
||||
t.Fatal("prefer phone")
|
||||
}
|
||||
now += phoneFixMaxAge
|
||||
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
|
||||
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
|
||||
t.Fatal("prefer phone falls back to comma once the phone fix expires")
|
||||
}
|
||||
|
||||
mode = GpsModeComma
|
||||
now += gpsModeRefresh
|
||||
*phone = append(*phone, phoneGpsWire(t, now, now))
|
||||
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
|
||||
t.Fatal("comma and car only keeps comma GPS")
|
||||
}
|
||||
now += internalFixMaxAge + 1
|
||||
*phone = append(*phone, phoneGpsWire(t, now, now))
|
||||
if _, ok = s.ReadSample(); ok {
|
||||
t.Fatal("comma and car only must never use the phone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneGpsModeIsRereadAtMostOnceASecond(t *testing.T) {
|
||||
now := uint64(10_000_000_000)
|
||||
reads := 0
|
||||
s, _, _ := testGpsSub(&now)
|
||||
s.mode = func() string { reads++; return GpsModeBackup }
|
||||
s.ReadSample()
|
||||
now += gpsModeRefresh - 1
|
||||
s.ReadSample()
|
||||
if reads != 1 {
|
||||
t.Fatalf("mode reread early: %d", reads)
|
||||
}
|
||||
now++
|
||||
s.ReadSample()
|
||||
if reads != 2 {
|
||||
t.Fatalf("mode not reread: %d", reads)
|
||||
}
|
||||
}
|
||||
@@ -45,3 +45,11 @@ func CarGpsReader(evt log.Event) (custom.StarPilotCarState_Gps, error) {
|
||||
}
|
||||
return state.Gps()
|
||||
}
|
||||
|
||||
func PhoneGpsReader(evt log.Event) (custom.StarPilotCarState_Gps, error) {
|
||||
navigation, err := evt.StarpilotNavigation()
|
||||
if err != nil {
|
||||
return custom.StarPilotCarState_Gps{}, err
|
||||
}
|
||||
return navigation.PhoneGps()
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ var ServiceQueueSize = map[string]int64{
|
||||
"selfdriveState": QUEUE_SIZE_SMALL,
|
||||
"gpsLocation": QUEUE_SIZE_SMALL,
|
||||
"starpilotCarState": QUEUE_SIZE_SMALL,
|
||||
"starpilotNavigation": QUEUE_SIZE_MEDIUM,
|
||||
"gpsLocationExternal": QUEUE_SIZE_SMALL,
|
||||
"liveLocationKalman": QUEUE_SIZE_SMALL,
|
||||
"gpsNMEA": QUEUE_SIZE_SMALL,
|
||||
|
||||
+1
-1
@@ -176,7 +176,7 @@ func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader
|
||||
pos := m.PosFromLocation(location)
|
||||
s.Position = pos
|
||||
// Unknown CAN direction or speed uncertainty cannot create confident road evidence.
|
||||
if sample.Source == cereal.GpsSourceCar && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
|
||||
if (sample.Source == cereal.GpsSourceCar || sample.Source == cereal.GpsSourcePhone) && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
|
||||
s.clearRoadDerived()
|
||||
s.roadFixMonoTime = 0
|
||||
s.RoadStatus = custom.MapdOut_SampleStatus_noMatch
|
||||
|
||||
Binary file not shown.
@@ -38,6 +38,8 @@ class Inputs:
|
||||
lane_direction: int = 0
|
||||
limit_status: int = 0
|
||||
geometry: LaneGeometry | None = None
|
||||
speed_min: float | None = None
|
||||
speed_max: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -85,6 +87,12 @@ class AngleController:
|
||||
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
|
||||
self.reset()
|
||||
return Command()
|
||||
speed_min = max(i.speed, 0.1) if i.speed_min is None else i.speed_min
|
||||
speed_max = max(i.speed, 0.1) if i.speed_max is None else i.speed_max
|
||||
if (not all(math.isfinite(v) for v in (speed_min, speed_max)) or
|
||||
not 0 < speed_min <= max(i.speed, 0.1) <= speed_max):
|
||||
self.reset()
|
||||
return Command()
|
||||
if i.steering_pressed and not self.press_last:
|
||||
self.preturned = abs(i.steering_angle) > 45.0
|
||||
self.press_last = i.steering_pressed
|
||||
@@ -152,7 +160,7 @@ class AngleController:
|
||||
# measured-curvature interval and the modern ISO envelope. If those bands
|
||||
# cannot meet the previous actuator ROC, send a neutral mode-0 frame.
|
||||
effective_speed = max(i.speed, 0.1)
|
||||
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(i.speed, 1.0) ** 2)
|
||||
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(speed_max, 1.0) ** 2)
|
||||
low_kappa, high_kappa = -cap, cap
|
||||
if i.speed > 9.0:
|
||||
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
|
||||
@@ -161,8 +169,10 @@ class AngleController:
|
||||
# gain (0.95..1.3), so the worst equivalent curvature remains bounded.
|
||||
lower_gain = bounds.GAIN_MAX if low_kappa >= 0.0 else bounds.GAIN_MIN
|
||||
upper_gain = bounds.GAIN_MIN if high_kappa >= 0.0 else bounds.GAIN_MAX
|
||||
lower = max(-bounds.PATH_MAX, low_kappa * effective_speed * lower_gain, self.path_last - roc)
|
||||
upper = min(-bounds.PATH_MIN, high_kappa * effective_speed * upper_gain, self.path_last + roc)
|
||||
lower_speed = speed_max if low_kappa >= 0.0 else speed_min
|
||||
upper_speed = speed_min if high_kappa >= 0.0 else speed_max
|
||||
lower = max(-bounds.PATH_MAX, low_kappa * lower_speed * lower_gain, self.path_last - roc)
|
||||
upper = min(-bounds.PATH_MIN, high_kappa * upper_speed * upper_gain, self.path_last + roc)
|
||||
# Bound the actual-angle equivalent derivative at the worst admitted gain.
|
||||
# 45ms core margin remains inside the native elapsed-time/50ms cap.
|
||||
jerk_delta = bounds.LATERAL_JERK / max(i.speed - 1.0, 1.0) ** 2 * bounds.CORE_STEER_PERIOD_S * bounds.GAIN_MIN * effective_speed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""StarPilot's CP adapter; model transport and saved Params stay in the host.
|
||||
|
||||
Only exact final CP words derived from the existing five classic/six CANFD
|
||||
extended profiles can select angle. AOL and Transit keep their current owner.
|
||||
extended profiles can select angle. Combined AOL retains its original Ford profile identity.
|
||||
"""
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
@@ -18,7 +18,7 @@ TRUCKS = CANFD - {CAR.FORD_ESCAPE_MK4_5, CAR.FORD_MUSTANG_MACH_E_MK1}
|
||||
|
||||
|
||||
def mode_word(cp, *, selected):
|
||||
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience != 0 or
|
||||
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
|
||||
cp.steerControlType != structs.CarParams.SteerControlType.angle or
|
||||
not cp.pcmCruise or len(cp.safetyConfigs) not in (1, 2)):
|
||||
return None
|
||||
@@ -32,7 +32,13 @@ def mode_word(cp, *, selected):
|
||||
safety = cp.safetyConfigs[-1]
|
||||
long = int(cp.openpilotLongitudinalControl)
|
||||
original = (18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else 66) + long if fd else 32 + long
|
||||
expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original
|
||||
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
|
||||
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
|
||||
return None
|
||||
if cp.alternativeExperience == 32:
|
||||
expected = original | (256 if manual else 0) | (ANGLE_FLAG if selected else 0)
|
||||
else:
|
||||
expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original
|
||||
return expected if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected else None
|
||||
|
||||
|
||||
@@ -49,7 +55,10 @@ def select(cp, raw):
|
||||
chosen = (type(raw) is int and raw == 1) or (type(raw) in (bytes, str) and raw in (b"1", "1"))
|
||||
if not chosen or mode_word(cp, selected=False) is None:
|
||||
return False
|
||||
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
|
||||
if cp.alternativeExperience == 32:
|
||||
cp.safetyConfigs[-1].safetyParam |= ANGLE_FLAG
|
||||
else:
|
||||
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
|
||||
cp.steerActuatorDelay = 0.22
|
||||
return True
|
||||
|
||||
@@ -79,16 +88,40 @@ class Host:
|
||||
self.cp = cp
|
||||
self.controller = AngleController(low_gain=low, high_gain=high)
|
||||
self.inputs = None
|
||||
self.tx_blocked = None
|
||||
|
||||
def update(self, CC, CS):
|
||||
if self.inputs is not None:
|
||||
self.inputs.update()
|
||||
snapshot = self.inputs.lateral_snapshot(CS.out.vEgoRaw) if self.inputs is not None else None
|
||||
model, times, delay, _ = snapshot if snapshot is not None else (None, (), 0.15, True)
|
||||
aol = self.cp.alternativeExperience == 32
|
||||
ordinary = CC.enabled and CS.out.cruiseState.enabled and not CS.out.gasPressed and not CS.out.brakePressed
|
||||
ready = (self.inputs is not None and self.inputs.native_permission(self.cp, max_age_ns=bounds.PANDA_AGE_NS) and
|
||||
CC.enabled and CC.latActive and CS.out.cruiseState.enabled and CS.out.canValid and
|
||||
not CS.out.canTimeout and not CS.out.gasPressed and not CS.out.brakePressed and
|
||||
(aol and CS.out.cruiseState.available or ordinary) and CC.latActive and CS.out.canValid and
|
||||
not CS.out.canTimeout and
|
||||
(not aol or CS.out.gearShifter in (structs.CarState.GearShifter.drive,
|
||||
structs.CarState.GearShifter.sport, structs.CarState.GearShifter.low)) and
|
||||
not CS.out.steerFaultTemporary and not CS.out.steerFaultPermanent and not CS.out.vehicleSensorsInvalid)
|
||||
# Rejected frames do not advance Panda's accepted counter/path history.
|
||||
# One real blocked-TX increment requires an accepted neutral resynchronization,
|
||||
# rather than sending an indefinitely rejected chain of active counters.
|
||||
counter_reader = getattr(self.inputs, "native_tx_blocked", None)
|
||||
blocked = counter_reader() if ready and callable(counter_reader) else None
|
||||
if not ready:
|
||||
self.tx_blocked = None
|
||||
elif type(blocked) is int and 0 <= blocked <= 0xFFFFFFFF:
|
||||
changed = self.tx_blocked is not None and blocked != self.tx_blocked
|
||||
self.tx_blocked = blocked
|
||||
ready = not changed
|
||||
speed_min = speed_max = None
|
||||
speed_window = getattr(CS, "bluepilot_speed_window", None)
|
||||
if speed_window is not None:
|
||||
low, high, step = speed_window
|
||||
# Allow source progress across the existing 100 ms CAN freshness window.
|
||||
margin = max(0.05, 5 * step)
|
||||
speed_min = max(0.1, min(low, CS.out.vEgoRaw) - margin)
|
||||
speed_max = max(high, CS.out.vEgoRaw, 0.1) + margin
|
||||
meta = model.meta if model is not None else None
|
||||
return self.controller.step(Inputs(
|
||||
active=ready, speed=CS.out.vEgoRaw, measured_curvature=-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1),
|
||||
@@ -98,4 +131,4 @@ class Host:
|
||||
lane_change=int(getattr(meta.laneChangeState, "raw", meta.laneChangeState)) in (1, 2, 3) if meta else False,
|
||||
lane_direction=int(getattr(meta.laneChangeDirection, "raw", meta.laneChangeDirection)) if meta else 0,
|
||||
limit_status=getattr(CS, "lat_ctl_lim_stat", 0),
|
||||
geometry=model_geometry(model, fresh=snapshot is not None)))
|
||||
geometry=model_geometry(model, fresh=snapshot is not None), speed_min=speed_min, speed_max=speed_max))
|
||||
|
||||
@@ -41,6 +41,9 @@ static bool ford_bp_configure(uint16_t param) {
|
||||
#ifdef ALLOW_DEBUG
|
||||
ford_bp_enabled |= (param == 131U) && (alternative_experience == 0);
|
||||
#endif
|
||||
const bool joint = ((unsigned int)alternative_experience == 32U) &&
|
||||
((param == 146U) || (param == 147U) || (param == 160U) || (param == 161U) || (param == 194U) || (param == 195U));
|
||||
ford_bp_enabled |= joint;
|
||||
ford_bp_canfd = (param & 2U) != 0U;
|
||||
ford_bp_lka_counter_seen = false;
|
||||
ford_bp_control_counter_seen = false;
|
||||
@@ -62,6 +65,16 @@ static bool ford_bp_configure(uint16_t param) {
|
||||
|
||||
static bool ford_bp_health(uint32_t now);
|
||||
|
||||
static bool ford_bp_permission(void) {
|
||||
bool permitted = false;
|
||||
if (ford_aol_enabled) {
|
||||
permitted = (ford_aol_permission_mask() & 1U) != 0U;
|
||||
} else {
|
||||
permitted = lateral_controls_allowed() && controls_allowed && !brake_pressed && !gas_pressed;
|
||||
}
|
||||
return permitted;
|
||||
}
|
||||
|
||||
static void ford_bp_rx(const CANPacket_t *msg) {
|
||||
if (ford_bp_enabled && (msg->bus == 0U) && (GET_LEN(msg) == 8U)) {
|
||||
const unsigned int ids[7] = {0x415U, 0x202U, 0x91U, 0x165U, 0x204U, 0x213U, 0x83U};
|
||||
@@ -77,7 +90,7 @@ static void ford_bp_rx(const CANPacket_t *msg) {
|
||||
controls_allowed = false;
|
||||
}
|
||||
// Permission losses remove the old actuator history before any later arm.
|
||||
if (!controls_allowed || brake_pressed || gas_pressed) {
|
||||
if (!ford_bp_permission() || !ford_bp_health(microsecond_timer_get())) {
|
||||
ford_bp_withdraw();
|
||||
}
|
||||
}
|
||||
@@ -113,7 +126,7 @@ static bool ford_bp_lka(const CANPacket_t *msg) {
|
||||
((msg->data[7] & FORD_BP_VERSION_MASK) == FORD_BP_VERSION) &&
|
||||
((msg->data[7] >> FORD_BP_CHECKSUM_SHIFT) == checksum);
|
||||
const int shadow = to_signed((msg->data[5] << 8) | msg->data[6], 16);
|
||||
const bool resync = (shadow == 0) && !controls_allowed;
|
||||
const bool resync = (shadow == 0) && !ford_bp_permission();
|
||||
if (ford_bp_lka_counter_seen && !resync) {
|
||||
valid &= counter == ((ford_bp_lka_counter + 1U) % FORD_BP_COUNTER_MODULUS);
|
||||
valid &= safety_get_ts_elapsed(now, ford_bp_shadow_us) >= 20000U;
|
||||
@@ -193,7 +206,7 @@ static bool ford_bp_lateral(const CANPacket_t *msg) {
|
||||
const float physical_cap = SAFETY_MIN(FORD_BP_CURVATURE_MAX, FORD_BP_LATERAL_ACCEL / (SAFETY_MAX(speed_max, 1.0F) * SAFETY_MAX(speed_max, 1.0F)));
|
||||
const float relation_low = SAFETY_ABS(shadow) * speed_min * FORD_BP_GAIN_MIN * 2000.0F;
|
||||
const float relation_high = SAFETY_ABS(shadow) * speed_max * FORD_BP_GAIN_MAX * 2000.0F;
|
||||
valid &= lateral_controls_allowed() && controls_allowed && ford_bp_health(now) && ford_bp_shadow_seen &&
|
||||
valid &= ford_bp_permission() && ford_bp_health(now) && ford_bp_shadow_seen &&
|
||||
(safety_get_ts_elapsed(now, ford_bp_shadow_us) <= FORD_BP_SHADOW_AGE_US) &&
|
||||
(path >= -1000) && (path <= 1047) && (SAFETY_ABS(path - ford_bp_path_last) <= delta) &&
|
||||
((path == 0) || ((ford_bp_shadow_raw != 0) && ((path > 0) == (ford_bp_shadow_raw > 0)))) &&
|
||||
|
||||
@@ -11,6 +11,7 @@ from opendbc.car.fw_versions import (ObdCallback, get_fw_versions, get_fw_versio
|
||||
from opendbc.car.mock.values import CAR as MOCK
|
||||
from opendbc.car.gm.pedal_capability import automatic_bolt_candidate
|
||||
from opendbc.car.gm.discovery import automatic_volt_candidate
|
||||
from opendbc.car.ford.discovery import normal_bus0_selection as ford_normal_bus0_selection, normal_bus0_queries as ford_normal_bus0_queries
|
||||
from opendbc.car.hyundai.discovery import normal_bus_topology
|
||||
from opendbc.car.values import BRANDS
|
||||
from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
|
||||
@@ -97,8 +98,10 @@ def _gm_fw_uses_normal_bus0() -> bool:
|
||||
|
||||
# **** for use live only ****
|
||||
def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback,
|
||||
cached_params: CarParamsT | None, *, known_gm_bus0: bool = False
|
||||
cached_params: CarParamsT | None, *, known_gm_bus0: bool = False, known_ford_bus0: bool = False
|
||||
) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
|
||||
if known_ford_bus0 and (known_gm_bus0 or not ford_normal_bus0_queries()):
|
||||
raise ValueError('Ford discovery topology no longer uses normal bus 0')
|
||||
if known_gm_bus0 and not _gm_fw_uses_normal_bus0():
|
||||
raise ValueError('GM discovery topology no longer uses normal bus 0')
|
||||
fixed_fingerprint = os.environ.get('FINGERPRINT', "")
|
||||
@@ -128,6 +131,18 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
|
||||
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0,))
|
||||
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing, query_brand='gm')
|
||||
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
|
||||
elif known_ford_bus0:
|
||||
# A registered CAN-FD selection identifies Ford's query family, not live
|
||||
# ECU authority. Keep normal CAN2 wiring during discovery: the unrelated
|
||||
# all-brand OBD mux scan can produce a CAN2 error/interrupt storm.
|
||||
set_obd_multiplexing(False)
|
||||
warmup_remaining = 0.1 - (time.monotonic() - start_time)
|
||||
if warmup_remaining > 0:
|
||||
time.sleep(warmup_remaining)
|
||||
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0,))
|
||||
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing,
|
||||
query_brand='ford', normal_bus_only=True)
|
||||
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
|
||||
elif normal_bus_topology(can_recv):
|
||||
|
||||
set_obd_multiplexing(False)
|
||||
@@ -198,12 +213,19 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
|
||||
cached_params = None
|
||||
explicit_candidate = forced_candidate if forced_candidate is not None else os.environ.get('FINGERPRINT', '')
|
||||
known_gm_bus0 = (explicit_candidate in interface_names['gm'] and _gm_fw_uses_normal_bus0())
|
||||
known_ford_bus0 = ford_normal_bus0_selection(explicit_candidate)
|
||||
if known_gm_bus0 and cached_params is not None and (
|
||||
cached_params.brand != 'gm' or cached_params.carFingerprint != explicit_candidate):
|
||||
cached_params = None
|
||||
if known_ford_bus0 and cached_params is not None and (
|
||||
cached_params.brand != 'ford' or cached_params.carFingerprint != explicit_candidate):
|
||||
cached_params = None
|
||||
if known_gm_bus0:
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(
|
||||
can_recv, can_send, set_obd_multiplexing, cached_params, known_gm_bus0=True)
|
||||
elif known_ford_bus0:
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(
|
||||
can_recv, can_send, set_obd_multiplexing, cached_params, known_ford_bus0=True)
|
||||
else:
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, cached_params)
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Exact stock-ACC Chrysler AOL admission. Dashcam variants remain excluded."""
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.chrysler.values import CAR, ChryslerFlags
|
||||
|
||||
CHRYSLER_AOL_EXPERIENCE = 32
|
||||
CHRYSLER_AOL_CLASSIC = frozenset((
|
||||
CAR.CHRYSLER_PACIFICA_2018_HYBRID, CAR.CHRYSLER_PACIFICA_2019_HYBRID,
|
||||
CAR.CHRYSLER_PACIFICA_2018, CAR.CHRYSLER_PACIFICA_2020, CAR.DODGE_DURANGO,
|
||||
CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019,
|
||||
))
|
||||
CHRYSLER_AOL_CARS = CHRYSLER_AOL_CLASSIC | {CAR.RAM_1500_5TH_GEN}
|
||||
|
||||
|
||||
def qualified(cp, *, marked_only=False):
|
||||
if (cp is None or cp.brand != 'chrysler' or cp.carFingerprint not in CHRYSLER_AOL_CARS or
|
||||
cp.notCar or cp.passive or cp.dashcamOnly or not cp.pcmCruise or
|
||||
cp.openpilotLongitudinalControl or cp.alphaLongitudinalAvailable or
|
||||
cp.steerControlType != structs.CarParams.SteerControlType.torque or
|
||||
len(cp.safetyConfigs) != 1 or
|
||||
cp.alternativeExperience not in ((CHRYSLER_AOL_EXPERIENCE,) if marked_only else (0, CHRYSLER_AOL_EXPERIENCE))):
|
||||
return False
|
||||
flags = ChryslerFlags.HIGHER_MIN_STEERING_SPEED | ChryslerFlags.HAS_BSM | ChryslerFlags.STEERING_SPEED_BYPASS
|
||||
jeep = cp.carFingerprint in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019)
|
||||
if jeep:
|
||||
flags |= ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE
|
||||
expected = (1 if cp.carFingerprint == CAR.RAM_1500_5TH_GEN else
|
||||
8 if cp.carFingerprint == CAR.CHRYSLER_PACIFICA_2019_HYBRID and cp.alternativeExperience == 32 else 0)
|
||||
return bool(not int(cp.flags) & ~int(flags) and
|
||||
cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.chrysler and
|
||||
int(cp.safetyConfigs[0].safetyParam) in ((0, 4) if jeep else (expected,)) and
|
||||
cp.lateralTuning.which() in (('torque',) if cp.carFingerprint == CAR.RAM_1500_5TH_GEN else ('pid', 'torque')))
|
||||
|
||||
|
||||
def temporary_restriction(cp, state):
|
||||
gears = structs.CarState.GearShifter
|
||||
return bool(qualified(cp, marked_only=True) and state is not None and
|
||||
state.canValid and not state.canTimeout and not state.steerFaultPermanent and
|
||||
(state.gearShifter not in (gears.drive, gears.low) or state.steerFaultTemporary or
|
||||
state.standstill or state.vEgo < cp.minSteerSpeed))
|
||||
|
||||
|
||||
def lkas_button_supported(cp):
|
||||
# Dom's Chrysler whitelist did not enable LKAS AOL. Preserve that feature
|
||||
# disposition until an intentional end-to-end activation requirement exists.
|
||||
return False
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Optional stock-ACC stop retention for exactly the two Grand Cherokee ports."""
|
||||
|
||||
|
||||
def brake_hold_supported(cp):
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.chrysler.values import CAR, ChryslerFlags
|
||||
return bool(cp is not None and cp.brand == "chrysler" and
|
||||
cp.carFingerprint in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019) and
|
||||
cp.flags & ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE and cp.pcmCruise and
|
||||
not cp.openpilotLongitudinalControl and not cp.passive and not cp.dashcamOnly and not cp.notCar and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.chrysler and
|
||||
cp.safetyConfigs[0].safetyParam in (0, 4))
|
||||
|
||||
|
||||
def apply_brake_hold(cp, requested):
|
||||
if brake_hold_supported(cp):
|
||||
cp.safetyConfigs[0].safetyParam = 4 if requested else 0
|
||||
|
||||
|
||||
def create_das_3_command(packer, counter_offset, brake_decel, das_3):
|
||||
values = das_3.copy()
|
||||
values.update(ACC_AVAILABLE=1, ACC_ACTIVE=1, ACC_GO=0, ACC_STANDSTILL=0, ACC_DECEL_REQ=1,
|
||||
ACC_DECEL=brake_decel, ACC_BRK_PREP=0, ENGINE_TORQUE_REQUEST_MAX=0, GR_MAX_REQ=2,
|
||||
COUNTER=(int(das_3["COUNTER"]) + counter_offset) % 16)
|
||||
return packer.make_can_msg("DAS_3", 0, values)
|
||||
@@ -1,7 +1,12 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, DT_CTRL
|
||||
from opendbc.car.lateral import apply_meas_steer_torque_limits
|
||||
from opendbc.car.chrysler import chryslercan
|
||||
from opendbc.car.chrysler.aol_contract import qualified as aol_qualified
|
||||
from opendbc.car.chrysler.brake_hold import brake_hold_supported, create_das_3_command
|
||||
from opendbc.car.chrysler.values import CUSW_CARS, RAM_CARS, RAM_DT, CarControllerParams, ChryslerFlags
|
||||
from opendbc.car.structs import CarState
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
@@ -16,6 +21,11 @@ class CarController(CarControllerBase):
|
||||
self.last_lkas_falling_edge = 0
|
||||
self.lkas_control_bit_prev = False
|
||||
self.last_button_frame = 0
|
||||
self.jeep_hold_replay = "REPLAY" in os.environ
|
||||
self.jeep_decel = -2.0
|
||||
self.last_das_3_counter = -1
|
||||
self.jeep_hold_cancelled = False
|
||||
self.jeep_cruise_was_active = False
|
||||
|
||||
self.packer = CANPacker(dbc_names[Bus.pt])
|
||||
self.params = CarControllerParams(CP)
|
||||
@@ -33,12 +43,17 @@ class CarController(CarControllerBase):
|
||||
das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0
|
||||
|
||||
# ACC cancellation
|
||||
if CC.cruiseControl.cancel:
|
||||
pause_stock_acc = bool(aol_qualified(self.CP, marked_only=True) and
|
||||
CS.aol_pause_longitudinal and 0 < CS.aol_pause_longitudinal_stamp_ns <= now_nanos and
|
||||
now_nanos - CS.aol_pause_longitudinal_stamp_ns <= 30_000_000 and
|
||||
CS.out.canValid and not CS.out.canTimeout)
|
||||
if CC.cruiseControl.cancel or (pause_stock_acc and CS.out.cruiseState.enabled):
|
||||
self.last_button_frame = self.frame
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True))
|
||||
|
||||
# ACC resume from standstill
|
||||
elif CC.cruiseControl.resume:
|
||||
elif (CC.cruiseControl.resume and not pause_stock_acc and
|
||||
(not aol_qualified(self.CP, marked_only=True) or CS.out.cruiseState.enabled)):
|
||||
self.last_button_frame = self.frame
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True))
|
||||
|
||||
@@ -94,6 +109,14 @@ class CarController(CarControllerBase):
|
||||
if _projection is not None:
|
||||
_projection.emit('chrysler.LKAS_COMMAND', apply_torque, lkas_control_bit, can_sends[-1])
|
||||
|
||||
if brake_hold_supported(self.CP) and self.CP.safetyConfigs[0].safetyParam == 4:
|
||||
withdraw = bool(aol_qualified(self.CP, marked_only=True) and CS.aol_pause_longitudinal and
|
||||
0 < CS.aol_pause_longitudinal_stamp_ns <= now_nanos and
|
||||
now_nanos - CS.aol_pause_longitudinal_stamp_ns <= 30_000_000)
|
||||
self.update_jeep_brake_hold(CC, CS, now_nanos, can_sends, withdraw=withdraw)
|
||||
else:
|
||||
CS.brake_hold = False
|
||||
|
||||
self.frame += 1
|
||||
|
||||
new_actuators = CC.actuators.as_builder()
|
||||
@@ -103,3 +126,43 @@ class CarController(CarControllerBase):
|
||||
if _projection is not None:
|
||||
_projection.finish(new_actuators.torque)
|
||||
return new_actuators, can_sends
|
||||
|
||||
def update_jeep_brake_hold(self, CC, CS, now_nanos, can_sends, *, withdraw=False):
|
||||
# CAN Event/parser timestamps are BOOTTIME. Card passes MONOTONIC for
|
||||
# ordinary live Chrysler controllers, and original CAN time during replay.
|
||||
# Select the established replay contract explicitly; never infer an offset
|
||||
# or renew the parser's original source timestamp at command time.
|
||||
now_boot_ns = now_nanos if self.jeep_hold_replay else time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
if (not CS.das_3 or not CS.out.canValid or CS.out.canTimeout or
|
||||
not 0 <= now_boot_ns - CS.das_3_timestamp <= 100_000_000):
|
||||
CS.brake_hold = False
|
||||
return
|
||||
counter = int(CS.das_3["COUNTER"])
|
||||
changed = counter != self.last_das_3_counter
|
||||
self.last_das_3_counter = counter
|
||||
active = CS.out.cruiseState.enabled
|
||||
if active and not self.jeep_cruise_was_active:
|
||||
self.jeep_hold_cancelled = False
|
||||
self.jeep_cruise_was_active = active
|
||||
if CC.cruiseControl.cancel or CS.jeep_driver_cancel or withdraw:
|
||||
self.jeep_hold_cancelled = True
|
||||
if self.jeep_hold_cancelled:
|
||||
CS.brake_hold = False
|
||||
return
|
||||
decel = float(CS.das_3["ACC_DECEL"])
|
||||
if not CS.brake_hold and CS.out.cruiseState.enabled and decel < -0.5 and CS.out.standstill:
|
||||
CS.brake_hold = True
|
||||
self.jeep_decel = -2.0
|
||||
if (CC.cruiseControl.cancel or CS.jeep_driver_cancel or withdraw or CS.out.gasPressed or CS.out.brakePressed or
|
||||
CS.out.gearShifter != CarState.GearShifter.drive or not CS.out.standstill or
|
||||
not CS.out.cruiseState.available or CS.out.accFaulted):
|
||||
CS.brake_hold = False
|
||||
return
|
||||
if not CS.brake_hold:
|
||||
return
|
||||
if CS.out.cruiseState.enabled:
|
||||
self.jeep_decel = max(-3.0, min(-0.5, min(self.jeep_decel, decel)))
|
||||
return
|
||||
can_sends.append(create_das_3_command(self.packer, 2 if changed else 3, self.jeep_decel, CS.das_3))
|
||||
if self.frame % 10 == 0:
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, 0, resume=True))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from opendbc.can import CANDefine, CANParser
|
||||
from opendbc.car import Bus, create_button_events, structs
|
||||
from opendbc.car.chrysler.aol_contract import qualified as aol_qualified
|
||||
from opendbc.car.chrysler.values import CUSW_CARS, DBC, STEER_THRESHOLD, RAM_CARS, ChryslerFlags
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import CarStateBase
|
||||
@@ -23,6 +24,13 @@ class CarState(CarStateBase):
|
||||
self.shifter_values = can_define.dv["GEAR"]["PRNDL"]
|
||||
|
||||
self.distance_button = 0
|
||||
self.aol_pause_longitudinal = False
|
||||
self.aol_pause_longitudinal_stamp_ns = 0
|
||||
self.aol_buttons = {ButtonType.mainCruise: 0, ButtonType.cancel: 0}
|
||||
self.brake_hold = False
|
||||
self.das_3 = {}
|
||||
self.das_3_timestamp = 0
|
||||
self.jeep_driver_cancel = False
|
||||
|
||||
def update(self, can_parsers) -> structs.CarState:
|
||||
cp = can_parsers[Bus.pt]
|
||||
@@ -80,6 +88,11 @@ class CarState(CarStateBase):
|
||||
ret.cruiseState.nonAdaptive = cp_cruise.vl["DAS_4"]["ACC_STATE"] in (1, 2) # 1 NormalCCOn and 2 NormalCCSet
|
||||
ret.cruiseState.standstill = cp_cruise.vl["DAS_3"]["ACC_STANDSTILL"] == 1
|
||||
ret.accFaulted = cp_cruise.vl["DAS_3"]["ACC_FAULTED"] != 0
|
||||
if self.CP.flags & ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE:
|
||||
self.das_3 = dict(cp_cruise.vl["DAS_3"])
|
||||
self.jeep_driver_cancel = bool(cp.vl["CRUISE_BUTTONS"]["ACC_Cancel"])
|
||||
self.das_3_timestamp = cp_cruise.ts_nanos["DAS_3"]["COUNTER"]
|
||||
ret.brakeHoldActive = self.brake_hold
|
||||
|
||||
if self.CP.carFingerprint in RAM_CARS:
|
||||
# Auto High Beam isn't Located in this message on chrysler or jeep currently located in 729 message
|
||||
@@ -97,7 +110,15 @@ class CarState(CarStateBase):
|
||||
self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
|
||||
self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
|
||||
|
||||
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
button_events = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
if aol_qualified(self.CP, marked_only=True):
|
||||
buttons = cp.vl["CRUISE_BUTTONS"]
|
||||
values = {ButtonType.mainCruise: bool(buttons["Cruise_OnOff"] or buttons["ACC_OnOff"]),
|
||||
ButtonType.cancel: bool(buttons["ACC_Cancel"])}
|
||||
for button, value in values.items():
|
||||
button_events += create_button_events(value, self.aol_buttons[button], {1: button})
|
||||
self.aol_buttons[button] = value
|
||||
ret.buttonEvents = button_events
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
elif candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
|
||||
ret.steerActuatorDelay = 0.2
|
||||
ret.flags |= ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE.value
|
||||
|
||||
ret.lateralTuning.init('pid')
|
||||
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Real Jeep parser/controller stock-stop retention; no system speed authority."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, gen_empty_fingerprint, structs
|
||||
from opendbc.car.chrysler.brake_hold import apply_brake_hold, brake_hold_supported
|
||||
from opendbc.car.chrysler.interface import CarInterface
|
||||
from opendbc.car.chrysler.values import CAR, DBC
|
||||
|
||||
|
||||
def params(candidate=CAR.JEEP_GRAND_CHEROKEE, *, enabled=True):
|
||||
cp = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False)
|
||||
apply_brake_hold(cp, enabled)
|
||||
return cp
|
||||
|
||||
|
||||
class PhysicalCan:
|
||||
def __init__(self, ci):
|
||||
ci.update([])
|
||||
self.ci = ci
|
||||
self.packers = {bus: CANPacker(parser.dbc.name) for bus, parser in ci.can_parsers.items()}
|
||||
|
||||
def frames(self, tick, *, active=True, decel=-2.0, main=True, gas=0.0, brake=0, speed=0.0, gear=4, fault=0, omit=()):
|
||||
values = {
|
||||
'DAS_3': {
|
||||
'ACC_ACTIVE': int(active),
|
||||
'ACC_AVAILABLE': int(main),
|
||||
'ACC_DECEL': decel,
|
||||
'ACC_STANDSTILL': 1,
|
||||
'ACC_FAULTED': fault,
|
||||
'ENGINE_TORQUE_REQUEST': 13.25,
|
||||
'DISABLE_FUEL_SHUTOFF': 1,
|
||||
'COLLISION_BRK_PREP': 1,
|
||||
'COUNTER': (14 + tick // 2) % 16,
|
||||
},
|
||||
'DAS_4': {'ACC_SET_SPEED_KPH': 100, 'ACC_STATE': 3},
|
||||
'GEAR': {'PRNDL': gear},
|
||||
'ECM_5': {'Accelerator_Position': gas},
|
||||
'ESP_1': {'Brake_Pedal_State': brake},
|
||||
'SPEED_1': {'SPEED_LEFT': speed, 'SPEED_RIGHT': speed},
|
||||
}
|
||||
return [
|
||||
self.packers[bus].make_can_msg(parser.dbc.addr_to_msg[address].name, parser.bus, values.get(parser.dbc.addr_to_msg[address].name, {}))
|
||||
for bus, parser in self.ci.can_parsers.items()
|
||||
for address in sorted(parser.addresses)
|
||||
if parser.dbc.addr_to_msg[address].name not in omit
|
||||
]
|
||||
|
||||
|
||||
def apply_at_boot(ci, command, now):
|
||||
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=now):
|
||||
return ci.apply(command.as_reader(), now)
|
||||
|
||||
|
||||
class TestJeepBrakeHold(unittest.TestCase):
|
||||
def setUp(self):
|
||||
prior = os.environ.pop('REPLAY', None)
|
||||
|
||||
def restore():
|
||||
if prior is None:
|
||||
os.environ.pop('REPLAY', None)
|
||||
else:
|
||||
os.environ['REPLAY'] = prior
|
||||
|
||||
self.addCleanup(restore)
|
||||
|
||||
def ready(self, candidate=CAR.JEEP_GRAND_CHEROKEE):
|
||||
ci = CarInterface(params(candidate))
|
||||
stream = PhysicalCan(ci)
|
||||
cc = structs.CarControl(enabled=True)
|
||||
for tick in range(20):
|
||||
ci.update([(100_000_000_000 + tick * 10_000_000, stream.frames(tick))])
|
||||
apply_at_boot(ci, cc, 100_000_000_000 + tick * 10_000_000)
|
||||
self.assertTrue(ci.CS.out.canValid and ci.CS.brake_hold)
|
||||
return ci, stream, cc
|
||||
|
||||
def test_two_exact_platforms_stock_ownership_default_off_and_siblings(self):
|
||||
for candidate in CAR:
|
||||
for release in (False, True):
|
||||
cp = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, release, False)
|
||||
expected = candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019)
|
||||
self.assertEqual(brake_hold_supported(cp), expected)
|
||||
before = cp.to_dict()
|
||||
apply_brake_hold(cp, True)
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, 4 if expected else before['safetyConfigs'][0]['safetyParam'])
|
||||
self.assertFalse(cp.openpilotLongitudinalControl)
|
||||
self.assertTrue(cp.pcmCruise)
|
||||
apply_brake_hold(cp, False)
|
||||
self.assertEqual(cp.to_dict(), before)
|
||||
|
||||
def test_real_parser_copied_fields_decel_counter_wrap_and_repeat(self):
|
||||
for candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
|
||||
ci, stream, cc = self.ready(candidate)
|
||||
decoder = CANParser(DBC[candidate][Bus.pt], [('DAS_3', 0)], 0)
|
||||
for tick in range(20, 55):
|
||||
now = 100_000_000_000 + tick * 10_000_000
|
||||
previous = ci.CC.last_das_3_counter
|
||||
ci.update([(now, stream.frames(tick, active=False))])
|
||||
_, frames = apply_at_boot(ci, cc, now)
|
||||
hold = [frame for frame in frames if frame[0] == 0x1F4]
|
||||
self.assertEqual(len(hold), 1)
|
||||
decoder.update([(now, hold)])
|
||||
parsed = decoder.vl['DAS_3']
|
||||
stock = ci.CS.das_3
|
||||
self.assertEqual(parsed['COUNTER'], (int(stock['COUNTER']) + (2 if int(stock['COUNTER']) != previous else 3)) % 16)
|
||||
for name in ('ENGINE_TORQUE_REQUEST', 'DISABLE_FUEL_SHUTOFF', 'COLLISION_BRK_PREP', 'ACC_FAULTED'):
|
||||
self.assertEqual(parsed[name], stock[name])
|
||||
for name, expected in {
|
||||
'ACC_ACTIVE': 1,
|
||||
'ACC_AVAILABLE': 1,
|
||||
'ACC_GO': 0,
|
||||
'ACC_STANDSTILL': 0,
|
||||
'ACC_DECEL_REQ': 1,
|
||||
'GR_MAX_REQ': 2,
|
||||
'ACC_BRK_PREP': 0,
|
||||
'ENGINE_TORQUE_REQUEST_MAX': 0,
|
||||
}.items():
|
||||
self.assertEqual(parsed[name], expected)
|
||||
self.assertAlmostEqual(parsed['ACC_DECEL'], -2.0, delta=0.004885)
|
||||
resumes = [frame for frame in frames if frame[0] == 0x23B]
|
||||
self.assertEqual(bool(resumes), tick % 10 == 0)
|
||||
|
||||
def test_driver_main_fault_and_source_withdrawal(self):
|
||||
for stimulus in ('gas', 'brake', 'speed', 'gear', 'main', 'fault', 'cancel', 'stale'):
|
||||
ci, stream, cc = self.ready()
|
||||
args = (
|
||||
{stimulus: {'gas': 1.0, 'brake': 1, 'speed': 1.0, 'gear': 1, 'main': False, 'fault': 1}.get(stimulus)} if stimulus not in ('cancel', 'stale') else {}
|
||||
)
|
||||
now = 100_200_000_000
|
||||
if stimulus == 'cancel':
|
||||
cc.cruiseControl.cancel = True
|
||||
ci.update([(now, stream.frames(20, active=False, **args))])
|
||||
if stimulus == 'stale':
|
||||
now += 100_000_001
|
||||
_, frames = apply_at_boot(ci, cc, now)
|
||||
self.assertFalse(ci.CS.brake_hold, stimulus)
|
||||
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames), stimulus)
|
||||
|
||||
def test_stock_braking_strength_clamped_to_original_envelope(self):
|
||||
ci, stream, cc = self.ready()
|
||||
for tick in range(20, 30):
|
||||
now = 100_000_000_000 + tick * 10_000_000
|
||||
ci.update([(now, stream.frames(tick, decel=-4.0))])
|
||||
apply_at_boot(ci, cc, now)
|
||||
self.assertEqual(ci.CC.jeep_decel, -3.0)
|
||||
|
||||
def test_real_parser_crc_fault_withdraws_hold(self):
|
||||
ci, stream, cc = self.ready()
|
||||
for tick in range(20, 32):
|
||||
now = 100_000_000_000 + tick * 10_000_000
|
||||
frames = stream.frames(tick, active=False)
|
||||
for index, (address, data, bus) in enumerate(frames):
|
||||
if address == 0x1F4:
|
||||
invalid = bytearray(data)
|
||||
invalid[-1] ^= 1
|
||||
frames[index] = (address, bytes(invalid), bus)
|
||||
ci.update([(now, frames)])
|
||||
_, emitted = apply_at_boot(ci, cc, now)
|
||||
self.assertGreater(now - ci.CS.das_3_timestamp, 100_000_000)
|
||||
self.assertFalse(ci.CS.brake_hold)
|
||||
self.assertFalse(any(frame[0] == 0x1F4 for frame in emitted))
|
||||
|
||||
def test_commanded_cancel_requires_fresh_stock_acc_engagement(self):
|
||||
ci, stream, cc = self.ready()
|
||||
cc.cruiseControl.cancel = True
|
||||
ci.update([(100_200_000_000, stream.frames(20))])
|
||||
apply_at_boot(ci, cc, 100_200_000_000)
|
||||
cc.cruiseControl.cancel = False
|
||||
for tick in range(21, 25):
|
||||
now = 100_000_000_000 + tick * 10_000_000
|
||||
ci.update([(now, stream.frames(tick))])
|
||||
apply_at_boot(ci, cc, now)
|
||||
self.assertFalse(ci.CS.brake_hold)
|
||||
ci.update([(100_250_000_000, stream.frames(25, active=False))])
|
||||
apply_at_boot(ci, cc, 100_250_000_000)
|
||||
ci.update([(100_260_000_000, stream.frames(26))])
|
||||
apply_at_boot(ci, cc, 100_260_000_000)
|
||||
self.assertTrue(ci.CS.brake_hold)
|
||||
|
||||
def test_actual_factory_default_off_and_siblings_emit_no_hold(self):
|
||||
for candidate in CAR:
|
||||
for requested in (False, True):
|
||||
if requested and candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
|
||||
continue
|
||||
ci = CarInterface(params(candidate, enabled=requested))
|
||||
stream = PhysicalCan(ci)
|
||||
cc = structs.CarControl()
|
||||
for tick in range(20):
|
||||
now = 100_000_000_000 + tick * 10_000_000
|
||||
ci.update([(now, stream.frames(tick, active=False))])
|
||||
_, frames = apply_at_boot(ci, cc, now)
|
||||
self.assertFalse(ci.CS.brake_hold)
|
||||
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
|
||||
|
||||
def test_actual_parser_reverse_then_drive_does_not_restore_disabled_stock_long(self):
|
||||
ci, stream, cc = self.ready()
|
||||
ci.update([(100_200_000_000, stream.frames(20, active=False, gear=2))])
|
||||
_, frames = apply_at_boot(ci, cc, 100_200_000_000)
|
||||
self.assertEqual(ci.CS.out.gearShifter, structs.CarState.GearShifter.reverse)
|
||||
self.assertFalse(ci.CS.brake_hold)
|
||||
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
|
||||
ci.update([(100_210_000_000, stream.frames(21, active=False, gear=4))])
|
||||
_, frames = apply_at_boot(ci, cc, 100_210_000_000)
|
||||
self.assertFalse(ci.CS.brake_hold)
|
||||
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
|
||||
|
||||
def test_original_can_boot_age_is_independent_of_controller_monotonic_time(self):
|
||||
ci, stream, cc = self.ready()
|
||||
boot_ns = 100_200_000_000
|
||||
ci.update([(boot_ns, stream.frames(20, active=False))])
|
||||
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=boot_ns):
|
||||
_, frames = ci.apply(cc.as_reader(), boot_ns - 9_000_000_000)
|
||||
self.assertTrue(ci.CS.brake_hold)
|
||||
self.assertTrue(any(frame[0] == 0x1F4 for frame in frames))
|
||||
|
||||
def test_original_boot_source_future_and_stale_are_not_restamped(self):
|
||||
for offset in (-1, 100_000_001):
|
||||
ci, stream, cc = self.ready()
|
||||
source = 100_200_000_000
|
||||
ci.update([(source, stream.frames(20, active=False))])
|
||||
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=source + offset):
|
||||
_, frames = ci.apply(cc.as_reader(), source)
|
||||
self.assertFalse(ci.CS.brake_hold)
|
||||
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
|
||||
|
||||
def test_replay_uses_original_can_time_in_the_explicit_replay_domain(self):
|
||||
with patch.dict(os.environ, {'REPLAY': '1'}):
|
||||
ci, stream, cc = self.ready()
|
||||
source = 100_200_000_000
|
||||
ci.update([(source, stream.frames(20, active=False))])
|
||||
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', side_effect=AssertionError('no host clock in original-time replay')):
|
||||
_, frames = ci.apply(cc.as_reader(), source)
|
||||
self.assertTrue(any(frame[0] == 0x1F4 for frame in frames))
|
||||
@@ -12,6 +12,8 @@ Ecu = CarParams.Ecu
|
||||
class ChryslerSafetyFlags(IntFlag):
|
||||
RAM_DT = 1
|
||||
RAM_HD = 2
|
||||
AOL_PACIFICA_HYBRID_SET_GUARD = 8
|
||||
JEEP_BRAKE_HOLD = 4
|
||||
|
||||
|
||||
class ChryslerFlags(IntFlag):
|
||||
@@ -19,6 +21,7 @@ class ChryslerFlags(IntFlag):
|
||||
HIGHER_MIN_STEERING_SPEED = 1
|
||||
HAS_BSM = 2 # blind spot monitoring
|
||||
STEERING_SPEED_BYPASS = 4
|
||||
JEEP_BRAKE_HOLD_AVAILABLE = 8
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,18 +1,56 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
|
||||
|
||||
AOL_EXPERIENCE = 32
|
||||
CLASSIC = frozenset((CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_FOCUS_MK4,
|
||||
CAR.FORD_MAVERICK_MK1, CAR.FORD_EXPLORER_MK6))
|
||||
GENERIC_CANFD = frozenset((CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2))
|
||||
AOL_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
|
||||
MANUAL = int(FordSafetyFlags.MANUAL)
|
||||
BASE_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
|
||||
ANGLE = 128
|
||||
ANGLE_BASE_WORDS = frozenset((18, 19, 32, 33, 66, 67))
|
||||
AOL_WORDS = (BASE_WORDS | frozenset(word | ANGLE for word in ANGLE_BASE_WORDS))
|
||||
AOL_WORDS |= frozenset(word | MANUAL for word in AOL_WORDS)
|
||||
|
||||
|
||||
def aol_safety_config(cp):
|
||||
if len(cp.safetyConfigs) not in (1, 2):
|
||||
return None
|
||||
if len(cp.safetyConfigs) == 2:
|
||||
passive = cp.safetyConfigs[0]
|
||||
if passive.safetyModel != structs.CarParams.SafetyModel.noOutput or passive.safetyParam != 0:
|
||||
return None
|
||||
owner = cp.safetyConfigs[-1]
|
||||
return owner if owner.safetyModel == structs.CarParams.SafetyModel.ford else None
|
||||
|
||||
|
||||
def base_safety_param(cp):
|
||||
owner = aol_safety_config(cp)
|
||||
if owner is None:
|
||||
return -1
|
||||
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
|
||||
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
|
||||
return -1
|
||||
word = int(owner.safetyParam)
|
||||
if (word & MANUAL and not manual) or (manual and cp.alternativeExperience == AOL_EXPERIENCE and not word & MANUAL):
|
||||
return -1
|
||||
if word & ANGLE:
|
||||
if cp.alternativeExperience == 0:
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
|
||||
if not angle_qualified(cp):
|
||||
return -1
|
||||
base = 18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else (66 if cp.carFingerprint in GENERIC_CANFD else 32)
|
||||
return base | int(cp.openpilotLongitudinalControl)
|
||||
if word not in AOL_WORDS:
|
||||
return -1
|
||||
word &= ~ANGLE
|
||||
return word & ~MANUAL
|
||||
|
||||
|
||||
def qualified(cp, *, marked_only=False):
|
||||
if (cp.brand != 'ford' or cp.carFingerprint not in tuple(CAR) or cp.passive or cp.notCar or cp.dashcamOnly or
|
||||
cp.steerControlType != structs.CarParams.SteerControlType.angle or
|
||||
cp.transmissionType != structs.CarParams.TransmissionType.automatic or len(cp.safetyConfigs) != 1 or
|
||||
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
|
||||
return False
|
||||
flags = int(cp.flags) & ~int(FordFlags.HAS_BSM)
|
||||
@@ -30,10 +68,9 @@ def qualified(cp, *, marked_only=False):
|
||||
base = 12
|
||||
else:
|
||||
return False
|
||||
safety = cp.safetyConfigs[0]
|
||||
return (cp.pcmCruise and
|
||||
safety.safetyModel == structs.CarParams.SafetyModel.ford and
|
||||
safety.safetyParam == (base | int(cp.openpilotLongitudinalControl)))
|
||||
safety = aol_safety_config(cp)
|
||||
return bool(cp.pcmCruise and safety is not None and
|
||||
base_safety_param(cp) == (base | int(cp.openpilotLongitudinalControl)))
|
||||
|
||||
|
||||
def temporary_restriction(cp, state):
|
||||
|
||||
@@ -75,6 +75,9 @@ class CarController(CarControllerBase):
|
||||
self.manual_turn_inputs = None
|
||||
self.mache_lateral = MachELateralController(CP) if mache_qualified(CP) else None
|
||||
self.mache_extended_announced = False
|
||||
self.mache_ui_owned = False
|
||||
self.hands_free_cluster_input = None
|
||||
self.cluster_extended_announced = False
|
||||
self.generic_canfd = generic_canfd_qualified(CP)
|
||||
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
|
||||
self.new_port_curvature = create_new_port_curvature(CP)
|
||||
@@ -98,6 +101,10 @@ class CarController(CarControllerBase):
|
||||
|
||||
def update(self, CC, CS, now_nanos):
|
||||
can_sends = []
|
||||
if self.hands_free_cluster_input is not None:
|
||||
self.hands_free_cluster_input.update()
|
||||
if not CC.latActive:
|
||||
self.mache_ui_owned = False
|
||||
if (self.mache_lateral is not None or self.classic_lateral is not None or self.new_port_curvature is not None) and self.manual_turn_inputs is not None:
|
||||
self.manual_turn_inputs.update()
|
||||
self.manual_turn_inputs.apply_blend_settings(self)
|
||||
@@ -138,6 +145,7 @@ class CarController(CarControllerBase):
|
||||
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.bp_lat is not None:
|
||||
can_sends += self.bp_lat.step_lateral(CC, CS, self.packer, self.CAN, self.frame, now_nanos)
|
||||
self.apply_curvature_last = self.bp_lat.apply_curvature_last
|
||||
self.mache_ui_owned |= bool(self.bp_lat.announced.active and self.bp_lat.pending.active)
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
|
||||
# Preserve the stock LMC heartbeat without requesting lateral action.
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, False, 0., 0., 0., 0.,
|
||||
@@ -156,6 +164,7 @@ class CarController(CarControllerBase):
|
||||
demanded = self.mache_lateral.update(CC, CS, actuators) if self.mache_extended_announced else FordLateralResult()
|
||||
lateral = bounded_command(self.mache_lateral, demanded, previous, CS.out.vEgoRaw, previous_path_angle)
|
||||
self.mache_lateral_demand = demanded
|
||||
self.mache_ui_owned |= bool(lateral.active)
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(mache_can.create_lat_ctl2_msg(
|
||||
@@ -322,25 +331,36 @@ class CarController(CarControllerBase):
|
||||
self.accel = accel
|
||||
self.gas = gas
|
||||
|
||||
# Track the actual outgoing LKA announcement for every CAN-FD display owner.
|
||||
if any(address == 0x3CA for address, _, _ in can_sends):
|
||||
self.cluster_extended_announced = True
|
||||
|
||||
### ui ###
|
||||
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert)
|
||||
# send lkas ui msg at 1Hz or if ui state changes
|
||||
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
|
||||
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
|
||||
# Acquire the display after the first active replacement, then retain it while
|
||||
# AOL stays active, including a manual-turn steering handoff (as on Dom).
|
||||
aol_mache_ui = self.CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and qualified_aol(self.CP, marked_only=True)
|
||||
ui_enabled = CC.latActive and (not aol_mache_ui or self.mache_ui_owned)
|
||||
if not aol_mache_ui or ui_enabled:
|
||||
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != ui_enabled) or (self.steer_alert_last != steer_alert)
|
||||
# send lkas ui msg at 1Hz or if ui state changes
|
||||
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
|
||||
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, ui_enabled, steer_alert, hud_control, CS.lkas_status_stock_values))
|
||||
|
||||
# send acc ui msg at 5Hz or if ui state changes
|
||||
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
|
||||
send_ui = True
|
||||
self.distance_bar_frame = self.frame
|
||||
# send acc ui msg at 5Hz or if ui state changes
|
||||
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
|
||||
send_ui = True
|
||||
self.distance_bar_frame = self.frame
|
||||
|
||||
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
|
||||
show_distance_bars = self.frame - self.distance_bar_frame < 400
|
||||
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
|
||||
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
|
||||
hud_control, CS.acc_tja_status_stock_values))
|
||||
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
|
||||
show_distance_bars = self.frame - self.distance_bar_frame < 400
|
||||
hands_free_cluster = bool(self.cluster_extended_announced and
|
||||
self.hands_free_cluster_input is not None and self.hands_free_cluster_input.enabled)
|
||||
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, ui_enabled,
|
||||
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
|
||||
hud_control, CS.acc_tja_status_stock_values, hands_free_cluster))
|
||||
|
||||
self.main_on_last = main_on
|
||||
self.lkas_enabled_last = CC.latActive
|
||||
self.lkas_enabled_last = ui_enabled
|
||||
self.steer_alert_last = steer_alert
|
||||
self.lead_distance_bars_last = hud_control.leadDistanceBars
|
||||
|
||||
|
||||
@@ -35,10 +35,13 @@ class CarState(CarStateBase):
|
||||
self.lc_button = 0
|
||||
self.cancel_button = False
|
||||
self.cancel_resume_button = FordStockCruiseButton()
|
||||
self.cruise_buttons = {ButtonType.accelCruise: False, ButtonType.decelCruise: False}
|
||||
self.steering_angle_offset_deg = 0.0
|
||||
self.lkas_available = False
|
||||
self.lkas_available_ts_nanos = 0
|
||||
self.lateral_motion_control = None
|
||||
self.bluepilot_speed_samples = [0] * 6 if CP.safetyConfigs and CP.safetyConfigs[-1].safetyParam & 128 else None
|
||||
self.bluepilot_speed_window = None
|
||||
|
||||
def get_car_gps(self):
|
||||
return self.car_gps_tracker.get()
|
||||
@@ -62,6 +65,14 @@ class CarState(CarStateBase):
|
||||
|
||||
# car speed
|
||||
ret.vEgoRaw = cp.vl["BrakeSysFeatures"]["Veh_V_ActlBrk"] * CV.KPH_TO_MS
|
||||
if self.bluepilot_speed_samples is not None:
|
||||
# Match Panda's six raw ABS speed samples, including its millimetre/second rounding.
|
||||
for value in cp.vl_all["BrakeSysFeatures"]["Veh_V_ActlBrk"]:
|
||||
self.bluepilot_speed_samples.append(math.floor(value * CV.KPH_TO_MS * 1000 + .5))
|
||||
self.bluepilot_speed_samples = self.bluepilot_speed_samples[-6:]
|
||||
samples = self.bluepilot_speed_samples
|
||||
step = max(abs(b - a) for a, b in zip(samples, samples[1:], strict=False)) / 1000
|
||||
self.bluepilot_speed_window = (min(samples) / 1000, max(samples) / 1000, step)
|
||||
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
|
||||
ret.yawRate = cp.vl["Yaw_Data_FD1"]["VehYaw_W_Actl"]
|
||||
ret.standstill = cp.vl["DesiredTorqBrk"]["VehStop_D_Stat"] == 1
|
||||
@@ -169,6 +180,13 @@ class CarState(CarStateBase):
|
||||
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
button_events += create_button_events(self.cancel_button, prev_cancel_button, {1: ButtonType.cancel})
|
||||
for button, signals in (
|
||||
(ButtonType.accelCruise, ('CcAslButtnSetIncPress', 'CcAslButtnResIncPress', 'CcAslButtnIndxIncPress')),
|
||||
(ButtonType.decelCruise, ('CcAslButtnSetDecPress', 'CcAslButtnResDecPress', 'CcAslButtnIndxDecPress')),
|
||||
):
|
||||
pressed = any(self.buttons_stock_values[signal] for signal in signals)
|
||||
button_events += create_button_events(pressed, self.cruise_buttons[button], {1: button})
|
||||
self.cruise_buttons[button] = pressed
|
||||
|
||||
ret.buttonEvents = button_events
|
||||
|
||||
@@ -207,6 +225,8 @@ class CarState(CarStateBase):
|
||||
gear = ("Gear_Shift_by_Wire_FD1" if CP.flags & FordFlags.CANFD else
|
||||
"TransGearData" if CP.flags & FordFlags.ALT_STEER_ANGLE else "PowertrainData_10")
|
||||
pt_messages.append((gear, 10))
|
||||
if CP.transmissionType == TransmissionType.manual:
|
||||
pt_messages.append(("BCM_Lamp_Stat_FD1", 10))
|
||||
if CP.flags & FordFlags.LKA_STEERING:
|
||||
pt_messages.append(("Lane_Assist_Data3_FD1", 30))
|
||||
cam_messages.append(("LateralMotionControl", 20))
|
||||
|
||||
@@ -6,6 +6,7 @@ CREDITS.md and THIRD_PARTY_NOTICES.md; this module only selects admitted callers
|
||||
"""
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.aol import base_safety_param
|
||||
from opendbc.car.ford.explorer_lateral import ExplorerLateralController, bounded_command as bounded_command, qualified as explorer_qualified
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralController
|
||||
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
|
||||
@@ -16,8 +17,6 @@ CLASSIC_EXTENDED_CARS = GENERIC_CLASSIC_CARS | {CAR.FORD_EXPLORER_MK6}
|
||||
|
||||
|
||||
def qualified(cp):
|
||||
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
|
||||
return False
|
||||
if cp.carFingerprint == CAR.FORD_EXPLORER_MK6:
|
||||
return explorer_qualified(cp)
|
||||
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CLASSIC_CARS or
|
||||
@@ -29,7 +28,7 @@ def qualified(cp):
|
||||
return False
|
||||
safety = cp.safetyConfigs[-1]
|
||||
expected = int(FordSafetyFlags.CLASSIC_EXTENDED | (FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
|
||||
|
||||
|
||||
class GenericClassicLateralController(FordLateralController):
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Normal-bus discovery for explicitly selected Ford CAN-FD vehicles."""
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
|
||||
|
||||
def normal_bus0_queries():
|
||||
# Verify both configured and effective requests before omitting the OBD sweep.
|
||||
# Essential queries use normal bus0 and its exact auxiliary Panda bus4 copy.
|
||||
# As-built remains bus0; bus1 logging cannot provide unique evidence.
|
||||
from opendbc.car import fw_versions
|
||||
configured = fw_versions.FW_QUERY_CONFIGS['ford'].requests
|
||||
effective = [request for brand, _, request in fw_versions.REQUESTS if brand == 'ford']
|
||||
if not configured or not effective:
|
||||
return False
|
||||
|
||||
def signature(request):
|
||||
return (tuple(request.request), tuple(request.response), request.rx_offset, frozenset(request.whitelist_ecus))
|
||||
for requests in (configured, effective):
|
||||
normal = {signature(request) for request in requests if request.bus == 0}
|
||||
essential = {signature(request) for request in requests if request.bus == 0 and not request.logging}
|
||||
auxiliary = {signature(request) for request in requests if request.bus == 4 and not request.logging}
|
||||
if not normal or not essential or auxiliary != essential:
|
||||
return False
|
||||
for request in requests:
|
||||
if request.bus == 0:
|
||||
continue
|
||||
if request.bus == 4 and not request.logging and signature(request) in essential:
|
||||
continue
|
||||
if request.bus == 1 and request.logging and signature(request) in normal:
|
||||
continue
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def normal_bus0_selection(candidate):
|
||||
return bool(candidate in CAR and CAR(candidate).config.flags & FordFlags.CANFD and
|
||||
not CAR(candidate).config.flags & FordFlags.LKA_STEERING and normal_bus0_queries())
|
||||
@@ -11,6 +11,7 @@ from typing import Protocol
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.aol import base_safety_param
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult, FORD_CURVATURE_LIMITS, STEER_DT
|
||||
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags, FordSafetyFlags
|
||||
|
||||
@@ -34,8 +35,6 @@ class ExplorerLateralController(FordLateralController):
|
||||
|
||||
|
||||
def qualified(CP) -> bool:
|
||||
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
|
||||
return False
|
||||
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_EXPLORER_MK6 or
|
||||
CP.flags & ~int(FordFlags.HAS_BSM) or CP.passive or CP.dashcamOnly or CP.notCar or
|
||||
CP.alternativeExperience not in (0, 32) or len(CP.safetyConfigs) not in (1, 2)):
|
||||
@@ -45,7 +44,7 @@ def qualified(CP) -> bool:
|
||||
return False
|
||||
safety = CP.safetyConfigs[-1]
|
||||
expected = int(FordSafetyFlags.EXPLORER_EXTENDED | (FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0))
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == expected
|
||||
|
||||
|
||||
class CurvatureOwner(Protocol):
|
||||
|
||||
@@ -32,10 +32,11 @@ def create_lka_msg(packer, CAN: CanBus, active=False, apply_angle=0., direction=
|
||||
if transit:
|
||||
values = {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0., "LdwActvIntns_D_Req": 3}
|
||||
if active:
|
||||
# 5.8 degrees rounds outside native raw 24..4072 at 0.05 mrad/count.
|
||||
values = {
|
||||
"LkaDrvOvrrd_D_Rq": 0,
|
||||
"LkaActvStats_D2_Req": direction,
|
||||
"LaRefAng_No_Req": math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.,
|
||||
"LaRefAng_No_Req": max(-101.2, min(101.2, math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.)),
|
||||
"LaRampType_B_Req": int(abs(apply_angle) >= 5.),
|
||||
"LaCurvature_No_Calc": max(-0.01023, min(0.01023, curvature)),
|
||||
"LdwActvStats_D_Req": 0,
|
||||
@@ -145,7 +146,7 @@ def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: fl
|
||||
|
||||
|
||||
def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool, standstill: bool,
|
||||
show_distance_bars: bool, hud_control, stock_values: dict):
|
||||
show_distance_bars: bool, hud_control, stock_values: dict, hands_free_cluster: bool = False):
|
||||
"""
|
||||
Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam
|
||||
assist status.
|
||||
@@ -161,6 +162,8 @@ def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw
|
||||
status = 3 # ActiveInterventionLeft
|
||||
elif hud_control.rightLaneDepart:
|
||||
status = 4 # ActiveInterventionRight
|
||||
elif hands_free_cluster:
|
||||
status = 7 # Hands-free assistance display
|
||||
else:
|
||||
status = 2 # Active
|
||||
elif main_on:
|
||||
|
||||
@@ -4,6 +4,7 @@ The reused BluePilot-derived strategy retains its original attribution in
|
||||
lateral_strategy.py, CREDITS.md and THIRD_PARTY_NOTICES.md.
|
||||
"""
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.aol import base_safety_param
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralController
|
||||
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
|
||||
@@ -14,8 +15,6 @@ SOURCE_ACCEL_CAP = 3.0 - 9.81 * .06
|
||||
|
||||
|
||||
def qualified(cp):
|
||||
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
|
||||
return False
|
||||
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CANFD_CARS or
|
||||
not cp.flags & FordFlags.CANFD or cp.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
|
||||
cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
|
||||
@@ -27,7 +26,7 @@ def qualified(cp):
|
||||
s = cp.safetyConfigs[-1]
|
||||
expected = int(FordSafetyFlags.GENERIC_CANFD_EXTENDED | FordSafetyFlags.CANFD |
|
||||
(FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
|
||||
return s.safetyModel == structs.CarParams.SafetyModel.ford and s.safetyParam == expected
|
||||
return s.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
|
||||
|
||||
|
||||
class GenericCanfdLateralController(FordLateralController):
|
||||
|
||||
@@ -55,12 +55,12 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
new_port = bool(ret.flags & FordFlags.NEW_PORT)
|
||||
if candidate in GENERIC_CANFD_CARS:
|
||||
ret.alphaLongitudinalAvailable = not is_release
|
||||
ret.alphaLongitudinalAvailable = True
|
||||
ret.steerActuatorDelay = 0.22
|
||||
longitudinal_requested = bool(alpha_long and not is_release)
|
||||
longitudinal_requested = bool(alpha_long)
|
||||
elif candidate == CAR.FORD_MUSTANG_MACH_E_MK1:
|
||||
ret.alphaLongitudinalAvailable = not is_release
|
||||
longitudinal_requested = bool(alpha_long and not is_release)
|
||||
ret.alphaLongitudinalAvailable = True
|
||||
longitudinal_requested = bool(alpha_long)
|
||||
elif candidate in CLASSIC_EXTENDED_CARS:
|
||||
# Original shared classic ownership is explicit stock/OP-long selection.
|
||||
# Actuator delay is live; modern LongControl already supplies the original P=0.
|
||||
@@ -68,7 +68,7 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.steerActuatorDelay = 0.22
|
||||
longitudinal_requested = bool(alpha_long)
|
||||
else:
|
||||
ret.alphaLongitudinalAvailable = (not is_release if ret.flags & FordFlags.CANFD else True) if new_port else ret.radarUnavailable
|
||||
ret.alphaLongitudinalAvailable = True if new_port else ret.radarUnavailable
|
||||
longitudinal_requested = bool(alpha_long and ret.alphaLongitudinalAvailable) if new_port else alpha_long or not ret.radarUnavailable
|
||||
if longitudinal_requested:
|
||||
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.LONG_CONTROL.value
|
||||
@@ -139,11 +139,6 @@ class CarInterface(CarInterfaceBase):
|
||||
and ret.safetyConfigs[-1].safetyParam in (2, 3)
|
||||
)
|
||||
if mach_e_extended:
|
||||
if is_release:
|
||||
# The native CANFD LONG profile is DEBUG-only; RELEASE stays stock.
|
||||
ret.alphaLongitudinalAvailable = False
|
||||
ret.openpilotLongitudinalControl = False
|
||||
ret.safetyConfigs[-1].safetyParam = FordSafetyFlags.CANFD.value
|
||||
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.MACH_E_EXTENDED.value
|
||||
if (candidate in GENERIC_CANFD_CARS and ret.flags & FordFlags.CANFD and
|
||||
not ret.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) and
|
||||
|
||||
@@ -17,6 +17,7 @@ from dataclasses import replace
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.aol import base_safety_param
|
||||
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
|
||||
from opendbc.car.lateral import MAX_LATERAL_JERK
|
||||
from opendbc.car.ford.lateral_strategy import (
|
||||
@@ -99,8 +100,6 @@ class MachELateralController(FordLateralController):
|
||||
|
||||
|
||||
def qualified(CP) -> bool:
|
||||
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
|
||||
return False
|
||||
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or
|
||||
not CP.flags & FordFlags.CANFD or CP.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
|
||||
CP.passive or CP.dashcamOnly or CP.notCar or CP.alternativeExperience not in (0, 32) or
|
||||
@@ -111,7 +110,7 @@ def qualified(CP) -> bool:
|
||||
return False
|
||||
safety = CP.safetyConfigs[-1]
|
||||
return (safety.safetyModel == structs.CarParams.SafetyModel.ford and
|
||||
safety.safetyParam == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
|
||||
base_safety_param(CP) == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
|
||||
(FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0)))
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from dataclasses import replace
|
||||
import math
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.aol import base_safety_param
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
@@ -22,7 +23,7 @@ def qualified(cp):
|
||||
if (profile is None or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or
|
||||
cp.alternativeExperience not in (0, 32) or not cp.pcmCruise or
|
||||
cp.steerControlType != structs.CarParams.SteerControlType.angle or
|
||||
len(cp.safetyConfigs) not in (1, 2) or (cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1)):
|
||||
len(cp.safetyConfigs) not in (1, 2)):
|
||||
return False
|
||||
flags, word = profile
|
||||
if (int(cp.flags) & ~int(FordFlags.HAS_BSM)) != flags:
|
||||
@@ -31,7 +32,7 @@ def qualified(cp):
|
||||
cp.safetyConfigs[0].safetyParam != 0):
|
||||
return False
|
||||
safety = cp.safetyConfigs[-1]
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == word + int(cp.openpilotLongitudinalControl)
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == word + int(cp.openpilotLongitudinalControl)
|
||||
|
||||
|
||||
class NewPortCurvatureController(FordLateralController):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Ford physical cancel/resume switch context; no new cruise authority."""
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.aol import base_safety_param
|
||||
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
|
||||
from opendbc.car.ford.classic_lateral import CLASSIC_EXTENDED_CARS
|
||||
from opendbc.car.ford.generic_canfd_lateral import qualified as generic_canfd_qualified
|
||||
@@ -30,8 +31,6 @@ def qualified(CP) -> bool:
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
|
||||
if angle_qualified(CP):
|
||||
return not CP.openpilotLongitudinalControl
|
||||
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
|
||||
return False
|
||||
"""Existing stock profiles with the paired physical-driver button contract."""
|
||||
if (CP.brand != "ford" or CP.carFingerprint not in tuple(CAR) or CP.openpilotLongitudinalControl or CP.alternativeExperience not in (0, 32) or
|
||||
CP.passive or CP.dashcamOnly or CP.notCar or len(CP.safetyConfigs) not in (1, 2) or
|
||||
@@ -41,17 +40,17 @@ def qualified(CP) -> bool:
|
||||
CP.safetyConfigs[0].safetyParam != 0):
|
||||
return False
|
||||
safety = CP.safetyConfigs[-1]
|
||||
if safety.safetyModel != structs.CarParams.SafetyModel.ford or safety.safetyParam not in (2, 8, 10, 12, 18, 32, 66):
|
||||
if safety.safetyModel != structs.CarParams.SafetyModel.ford or base_safety_param(CP) not in (2, 8, 10, 12, 18, 32, 66):
|
||||
return False
|
||||
if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == 32:
|
||||
if safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == 32:
|
||||
return CP.carFingerprint in CLASSIC_EXTENDED_CARS and not CP.flags & ~int(FordFlags.HAS_BSM)
|
||||
if safety.safetyParam == 66:
|
||||
if base_safety_param(CP) == 66:
|
||||
return generic_canfd_qualified(CP)
|
||||
expected = (int(FordSafetyFlags.CANFD) if CP.flags & FordFlags.CANFD else 0)
|
||||
expected |= int(FordSafetyFlags.NEW_PORT) if CP.flags & FordFlags.NEW_PORT else 0
|
||||
expected |= int(FordSafetyFlags.LKA_STEERING) if CP.flags & FordFlags.LKA_STEERING else 0
|
||||
if safety.safetyParam == 18:
|
||||
if base_safety_param(CP) == 18:
|
||||
if CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or expected != int(FordSafetyFlags.CANFD):
|
||||
return False
|
||||
expected |= int(FordSafetyFlags.MACH_E_EXTENDED)
|
||||
return safety.safetyParam == expected
|
||||
return base_safety_param(CP) == expected
|
||||
|
||||
@@ -20,18 +20,24 @@ PROFILES = (
|
||||
|
||||
@pytest.mark.parametrize('identity,base_word', PROFILES)
|
||||
@pytest.mark.parametrize('alpha', (False, True))
|
||||
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, monkeypatch):
|
||||
@pytest.mark.parametrize('topology', ('automatic', 'manual', 'multi-panda', 'manual-multi'))
|
||||
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, topology, monkeypatch):
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
||||
from opendbc.car.ford.tests.test_three_ports import params
|
||||
|
||||
monkeypatch.setenv('REPLAY', '1')
|
||||
monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0')
|
||||
cp = params(identity, alpha, False)
|
||||
offset = 4 if topology in ('multi-panda', 'manual-multi') else 0
|
||||
manual = topology in ('manual', 'manual-multi')
|
||||
fp = gen_empty_fingerprint()
|
||||
if not manual:
|
||||
fp[offset][0x5A] = 8
|
||||
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
|
||||
cp = CarInterface.get_params(identity, fp, [], alpha, True, False)
|
||||
assert cp.alternativeExperience == 0
|
||||
assert cp.safetyConfigs[0].safetyParam == base_word | int(alpha)
|
||||
assert cp.safetyConfigs[-1].safetyParam == base_word | int(alpha)
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
saved.put_bool('AlwaysOnLateral', False, block=True)
|
||||
@@ -67,15 +73,17 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
|
||||
if topology != "manual":
|
||||
fingerprint[offset][0x5A] = 8
|
||||
fingerprint[offset + 2].update({0x3D6: 8, 0x186: 8})
|
||||
expected_long = alpha and not release
|
||||
expected_word = base_word | int(expected_long)
|
||||
expected_long = alpha
|
||||
initial_word = base_word | int(expected_long)
|
||||
expected_word = initial_word | (256 if topology == "manual" else 0)
|
||||
initial = CarInterface.get_params(identity, fingerprint, [], expected_long, release, False)
|
||||
initial.carVin = "0" * 17
|
||||
initial.carFw = []
|
||||
admitted = topology == "automatic"
|
||||
admitted = True
|
||||
expected = initial.to_dict()
|
||||
expected["alternativeExperience"] = 32 if admitted else 0
|
||||
assert initial.safetyConfigs[-1].safetyParam == expected_word
|
||||
expected["safetyConfigs"][-1]["safetyParam"] = expected_word
|
||||
assert initial.safetyConfigs[-1].safetyParam == initial_word
|
||||
assert initial.openpilotLongitudinalControl == expected_long
|
||||
assert not initial.passive and not initial.dashcamOnly
|
||||
with OpenpilotPrefix():
|
||||
@@ -145,7 +153,7 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
|
||||
controls.publish(command, lateral_log)
|
||||
ci.apply(command.as_reader(), 1_000_000_000)
|
||||
if admitted:
|
||||
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, SafetyState, encode_safety
|
||||
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, PandaSlot, SafetyState, encode_safety
|
||||
|
||||
for tick, (acknowledged, stale) in enumerate(((False, False), (True, False), (True, True)), 1):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
@@ -160,8 +168,11 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
|
||||
axis.aolAxisState.nativeAcknowledged = acknowledged
|
||||
native = messaging.new_message(SAFETY_SERVICE, 0, valid=True, logMonoTime=now)
|
||||
native.aolSafetyWire = encode_safety(SafetyState(
|
||||
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[0].safetyModel.raw),
|
||||
card.CP.safetyConfigs[0].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current'))
|
||||
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[-1].safetyModel.raw),
|
||||
card.CP.safetyConfigs[-1].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current', sourcePandaStatesMonoTime=now,
|
||||
pandaInventory=tuple(PandaSlot(index, 'fixture-panda' if index == len(card.CP.safetyConfigs) - 1 else 'passive-panda',
|
||||
int(config.safetyModel.raw), config.safetyParam, 32, False, False)
|
||||
for index, config in enumerate(card.CP.safetyConfigs))))
|
||||
controls.sm.update_msgs(now / 1e9, [axis.as_reader(), native.as_reader()])
|
||||
active_command, _ = controls.state_control()
|
||||
assert active_command.enabled
|
||||
@@ -182,7 +193,7 @@ def test_actual_card_aol_publication_and_disabled_controls(identity, word, alpha
|
||||
|
||||
@pytest.mark.parametrize("identity,word", PROFILES)
|
||||
@pytest.mark.parametrize("topology", ("manual", "multi-panda"))
|
||||
def test_aol_denied_topology_preserves_actual_ordinary_startup(identity, word, topology, monkeypatch):
|
||||
def test_aol_manual_and_multi_panda_keep_actual_ordinary_startup(identity, word, topology, monkeypatch):
|
||||
run_startup(identity, word, False, False, topology, monkeypatch)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.car.card import Car, alpha_long_requested
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.starpilot.controller_extensions import ManualTurnInputs
|
||||
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
||||
@@ -34,7 +34,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
|
||||
saved.put_bool('FordHumanTurnDetection', False, block=True)
|
||||
for key, value in (("FordCurvatureBlendLow", 0.25), ("FordCurvatureBlendHigh", 0.65), ("FordCurvatureLaneChangeFactor", 1.1)):
|
||||
saved.put(key, value, block=True)
|
||||
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
|
||||
cp = params(identity, alpha, release)
|
||||
cp.carVin = '0' * 17
|
||||
cp.carFw = []
|
||||
expected = cp.to_dict()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Dom CAN-FD cluster scope through actual factory, setting UI and controller."""
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.starpilot.car.ford.cluster_preferences import KEY, qualified
|
||||
from openpilot.starpilot.controller_extensions import configure_controller
|
||||
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
|
||||
from openpilot.starpilot.ui.feature_settings_state import row_change
|
||||
|
||||
DOM_CANFD = (
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1, CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4,
|
||||
CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2, CAR.FORD_MONDEO_MK5,
|
||||
)
|
||||
|
||||
|
||||
def test_scope_covers_every_dom_canfd_profile():
|
||||
assert set(DOM_CANFD) == {car for car in CAR if car.config.flags & FordFlags.CANFD and not car.config.flags & FordFlags.LKA_STEERING}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', DOM_CANFD)
|
||||
@pytest.mark.parametrize('aol', [False, True])
|
||||
@pytest.mark.parametrize('enabled', [False, True])
|
||||
def test_all_dom_profiles_have_shared_setting_and_correct_active_status(identity, aol, enabled):
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
controller, state = setup(aol=aol, identity=identity)
|
||||
assert qualified(controller.CP)
|
||||
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
|
||||
vehicle_fingerprint=lambda: str(identity))
|
||||
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
|
||||
assert row.available and row.value == 'Off'
|
||||
if enabled:
|
||||
assert owner.apply(row_change(row, 1))
|
||||
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
|
||||
assert controller.hands_free_cluster_input.enabled == enabled
|
||||
command = structs.CarControl()
|
||||
for _ in range(10):
|
||||
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
|
||||
if identity == CAR.FORD_MUSTANG_MACH_E_MK1 and aol:
|
||||
assert not UI.intersection(address for address, _, _ in frames)
|
||||
command.latActive = True
|
||||
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
|
||||
statuses = []
|
||||
for _ in range(110):
|
||||
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
|
||||
for frame in frames:
|
||||
if frame[0] == 0x18A:
|
||||
parser.update([(controller.frame * 10_000_000, [frame])])
|
||||
statuses.append(parser.vl['ACCDATA_3']['Tja_D_Stat'])
|
||||
assert statuses and set(statuses) == {7 if enabled else 2}
|
||||
assert controller.cluster_extended_announced
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', [car for car in CAR if car not in DOM_CANFD])
|
||||
def test_non_canfd_fords_do_not_show_cluster_setting(identity):
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
controller, _ = setup(identity=identity)
|
||||
assert not qualified(controller.CP)
|
||||
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
|
||||
assert controller.hands_free_cluster_input is None
|
||||
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
|
||||
vehicle_fingerprint=lambda: str(identity))
|
||||
assert KEY not in {r.key for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows}
|
||||
|
||||
|
||||
def test_invalid_profile_does_not_enable_display_preference():
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
Path(saved.get_param_path(KEY)).write_bytes(b'1')
|
||||
controller, _ = setup()
|
||||
controller.CP.safetyConfigs[0].safetyParam = 0
|
||||
assert not qualified(controller.CP)
|
||||
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
|
||||
assert controller.hands_free_cluster_input is None
|
||||
@@ -10,14 +10,14 @@ from opendbc.car.ford.values import CAR, FordFlags
|
||||
|
||||
@pytest.mark.parametrize("car", sorted(GENERIC_CANFD_CARS))
|
||||
@pytest.mark.parametrize("alpha,release", ((False,False),(True,False),(False,True),(True,True)))
|
||||
def test_actual_profile_and_release_long_denial(car, alpha, release):
|
||||
def test_actual_profile_and_release_long_support(car, alpha, release):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[0][0x5A] = 8
|
||||
fp[2].update({0x3D6:8,0x186:8})
|
||||
cp = CarInterface.get_params(car, fp, [], alpha, release, False)
|
||||
assert cp.alphaLongitudinalAvailable == (not release)
|
||||
assert cp.openpilotLongitudinalControl == (alpha and not release)
|
||||
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha and not release else 66)
|
||||
assert cp.alphaLongitudinalAvailable
|
||||
assert cp.openpilotLongitudinalControl == alpha
|
||||
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha else 66)
|
||||
assert cp.steerActuatorDelay == pytest.approx(.22)
|
||||
assert qualified(cp)
|
||||
|
||||
@@ -212,8 +212,8 @@ def test_mache_camera_source_preserves_exact_factory_words(alpha, release, bsm):
|
||||
if bsm:
|
||||
fp[0].update({0x3A6: 8, 0x3A7: 8})
|
||||
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fp, [], alpha, release, False)
|
||||
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha and not release else 18)
|
||||
assert cp.openpilotLongitudinalControl == (alpha and not release)
|
||||
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha else 18)
|
||||
assert cp.openpilotLongitudinalControl == alpha
|
||||
assert not cp.radarUnavailable
|
||||
radar = RadarInterface(cp)
|
||||
assert radar.rcp.bus == 2 and radar.generic_canfd_lead
|
||||
|
||||
@@ -14,15 +14,15 @@ def fingerprint(offset=0, secoc=False):
|
||||
|
||||
|
||||
class TestMachEProfile(unittest.TestCase):
|
||||
def test_actual_params_stock_debug_long_and_release_stock(self):
|
||||
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 18)):
|
||||
def test_actual_params_stock_debug_long_and_release_long(self):
|
||||
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 19)):
|
||||
with self.subTest(alpha=alpha, release=release):
|
||||
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(), [], alpha, release, False)
|
||||
self.assertEqual(cp.safetyConfigs[-1].safetyParam, word)
|
||||
self.assertEqual(cp.openpilotLongitudinalControl, word == 19)
|
||||
self.assertEqual(cp.alternativeExperience, 0)
|
||||
if release:
|
||||
self.assertFalse(cp.alphaLongitudinalAvailable)
|
||||
self.assertTrue(cp.alphaLongitudinalAvailable)
|
||||
|
||||
def test_secoc_unrelated_flags_and_ae_do_not_select_extension(self):
|
||||
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(secoc=True), [], False, False, False)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Mach-E display preference, warning priority, and ownership regressions."""
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford import fordcan
|
||||
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
|
||||
from opendbc.car.ford.values import CAR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.starpilot.car.ford.cluster_preferences import KEY, FordClusterPreference, read_choice
|
||||
from openpilot.starpilot.controller_extensions import configure_controller
|
||||
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
|
||||
from openpilot.starpilot.ui.feature_settings_state import row_change
|
||||
|
||||
|
||||
def status(owner, state, command, parser):
|
||||
for _ in range(30):
|
||||
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
for address, data, bus in frames:
|
||||
if address == 0x18A:
|
||||
parser.update([(owner.frame * 10_000_000, [(address, data, bus)])])
|
||||
return parser.vl['ACCDATA_3']
|
||||
raise AssertionError('No active cluster message')
|
||||
|
||||
|
||||
def test_saved_opt_in_through_actual_controller_configuration_and_warning_priority():
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
owner, state = setup()
|
||||
configure_controller(SimpleNamespace(CP=owner.CP, CC=owner), saved)
|
||||
assert isinstance(owner.hands_free_cluster_input, FordClusterPreference)
|
||||
assert not owner.hands_free_cluster_input.enabled
|
||||
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
|
||||
command = structs.CarControl(latActive=True)
|
||||
state.acc_tja_status_stock_values['FcwAudioWarn_B_Rq'] = 1
|
||||
assert status(owner, state, command, parser)['Tja_D_Stat'] == 2
|
||||
Path(saved.get_param_path(KEY)).write_bytes(b'1')
|
||||
for _ in range(101):
|
||||
owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
assert status(owner, state, command, parser)['Tja_D_Stat'] == 7
|
||||
command.hudControl.leftLaneDepart = True
|
||||
assert status(owner, state, command, parser)['Tja_D_Stat'] == 3
|
||||
command.hudControl.leftLaneDepart = False
|
||||
command.hudControl.rightLaneDepart = True
|
||||
assert status(owner, state, command, parser)['Tja_D_Stat'] == 4
|
||||
command.hudControl.rightLaneDepart = False
|
||||
fields = status(owner, state, command, parser)
|
||||
assert fields['FcwAudioWarn_B_Rq'] == 1
|
||||
command.hudControl.visualAlert = structs.CarControl.HUDControl.VisualAlert.fcw
|
||||
assert status(owner, state, command, parser)['FcwVisblWarn_B_Rq'] == 1
|
||||
command.latActive = False
|
||||
for _ in range(110):
|
||||
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
assert not UI.intersection(address for address, _, _ in frames)
|
||||
Path(saved.get_param_path(KEY)).write_bytes(b'0')
|
||||
command.latActive = True
|
||||
for _ in range(101):
|
||||
owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
assert status(owner, state, command, parser)['Tja_D_Stat'] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw', [None, b'0', b'1', b'yes', b'1\n', b'', b'1' * 20])
|
||||
def test_preference_only_accepts_exact_dom_opt_in(raw):
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
if raw is not None:
|
||||
Path(saved.get_param_path(KEY)).write_bytes(raw)
|
||||
enabled, observed, _, _ = read_choice(saved)
|
||||
assert enabled == (raw == b'1')
|
||||
assert observed == (b'' if raw is not None and len(raw) > 8 else raw)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('enabled,main,left,right,expected', [
|
||||
(True, True, False, False, 7), (True, True, True, False, 3), (True, True, False, True, 4),
|
||||
(True, True, True, True, 3), (False, True, False, False, 1),
|
||||
(False, True, True, False, 5), (False, True, False, True, 6), (False, False, False, False, 0),
|
||||
])
|
||||
def test_hands_free_flag_does_not_override_departure_standby_or_off(enabled, main, left, right, expected):
|
||||
owner, state = setup()
|
||||
hud = structs.CarControl.HUDControl(leftLaneDepart=left, rightLaneDepart=right)
|
||||
frame = fordcan.create_acc_ui_msg(owner.packer, owner.CAN, owner.CP, main, enabled, False, False, False,
|
||||
hud, state.acc_tja_status_stock_values, True)
|
||||
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
|
||||
parser.update([(1, [frame])])
|
||||
assert parser.vl['ACCDATA_3']['Tja_D_Stat'] == expected
|
||||
|
||||
|
||||
def test_shared_feature_ui_saves_opt_in_and_rejects_onroad_stale_or_wrong_vehicle():
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
controller, _ = setup()
|
||||
parked = [True]
|
||||
owner = FeatureSettingsOwner(saved, lambda group: parked[0],
|
||||
vehicle_params=lambda: controller.CP,
|
||||
vehicle_fingerprint=lambda: str(controller.CP.carFingerprint))
|
||||
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
|
||||
assert row.value == 'Off' and row.available
|
||||
request = row_change(row, 1)
|
||||
assert request is not None
|
||||
parked[0] = False
|
||||
assert not owner.apply(request)
|
||||
parked[0] = True
|
||||
assert not owner.apply(replace(request, vehicle_fingerprint=str(CAR.FORD_F_150_MK14)))
|
||||
assert owner.apply(request)
|
||||
assert read_choice(saved)[0] == 1
|
||||
assert not owner.apply(request)
|
||||
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
|
||||
assert row.value == 'On'
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Recorded Mach-E turn inputs retain Dom display state across steering handoff."""
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.ford.carcontroller import CarController
|
||||
from opendbc.car.ford.carstate import CarState
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
from opendbc.car.ford.values import CAR, DBC
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests import test_ford_aol
|
||||
from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.starpilot.car.ford.cluster_preferences import FordClusterPreference, KEY
|
||||
|
||||
|
||||
class RecordedPreview:
|
||||
def __init__(self):
|
||||
self.model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.] * 33),
|
||||
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def apply_blend_settings(self, controller):
|
||||
controller.mache_lateral.set_blend_settings(.4, .4, .85)
|
||||
|
||||
def lateral_snapshot(self, speed):
|
||||
return self.model, ModelConstants.T_IDXS, .2, True
|
||||
|
||||
def assist_permission(self):
|
||||
return (), False
|
||||
|
||||
|
||||
def run_turn(*, hands_free, angle, torque, desired, speed, yaw):
|
||||
native = test_ford_aol.TestFordAolDriverIntent()
|
||||
native.setUp()
|
||||
native.reset(18)
|
||||
native.arm()
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
Path(saved.get_param_path(KEY)).write_bytes(b"1" if hands_free else b"0")
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[0].update({0x5A: 8, 0x3A6: 8, 0x3A7: 8})
|
||||
fingerprint[2].update({0x3D6: 8, 0x186: 8})
|
||||
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint, [], False, False, False)
|
||||
cp.alternativeExperience = 32
|
||||
assert cp.safetyConfigs[-1].safetyParam == 18
|
||||
assert cp.flags == 3
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
controller.hands_free_cluster_input = FordClusterPreference(saved)
|
||||
controller.manual_turn_inputs = RecordedPreview()
|
||||
state = CarState(cp)
|
||||
state.update(state.get_can_parsers(cp))
|
||||
cs = structs.CarState(canValid=True, vEgo=speed, vEgoRaw=speed, yawRate=yaw,
|
||||
gearShifter=structs.CarState.GearShifter.drive)
|
||||
cs.cruiseState.available = True
|
||||
command = structs.CarControl(latActive=False)
|
||||
command.actuators.curvature = desired
|
||||
state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] = 1
|
||||
state.acc_tja_status_stock_values['FcwAudioWarn_B_Rq'] = 1
|
||||
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
|
||||
steering = []
|
||||
displays = []
|
||||
|
||||
def step():
|
||||
native.pump(1, mask=int(command.latActive), speed=speed, curvature=yaw / speed)
|
||||
state.out = cs.as_reader()
|
||||
_, frames = controller.update(command.as_reader(), state, (1_000_000 + native.tick * 10_000) * 1000)
|
||||
for address, data, bus in frames:
|
||||
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
if address == 0x3D6:
|
||||
steering.append(bytes(data))
|
||||
if address == 0x18A:
|
||||
parser.update([(native.tick * 10_000_000, [(address, data, bus)])])
|
||||
displays.append(dict(parser.vl['ACCDATA_3']))
|
||||
return frames
|
||||
|
||||
# Startup remains stock until an active replacement is actually accepted.
|
||||
for _ in range(25):
|
||||
assert not {address for address, _, _ in step()} & {0x18A, 0x3D8}
|
||||
assert native.safety.safety_fwd_hook(2, 0x18A) == 0
|
||||
native.arm()
|
||||
command.latActive = True
|
||||
for _ in range(60):
|
||||
step()
|
||||
assert displays and displays[-1]['Tja_D_Stat'] == (7 if hands_free else 2)
|
||||
assert native.safety.aol_get_permission_mask() == 1
|
||||
assert native.safety.safety_fwd_hook(2, 0x18A) == -1
|
||||
|
||||
# Both original onset tuples have driver input, a right signal and >12deg.
|
||||
cs.steeringPressed = True
|
||||
cs.steeringAngleDeg = angle
|
||||
cs.steeringTorque = torque
|
||||
cs.rightBlinker = True
|
||||
count = len(displays)
|
||||
for _ in range(40):
|
||||
frames = step()
|
||||
if any(address == 0x3D6 for address, _, _ in frames):
|
||||
assert ((steering[-1][0] >> 4) & 7) == 0
|
||||
assert native.safety.safety_fwd_hook(2, 0x3D6) == 0
|
||||
assert native.safety.safety_fwd_hook(2, 0x18A) == -1
|
||||
assert native.safety.safety_fwd_hook(2, 0x3D8) == -1
|
||||
assert native.safety.aol_get_permission_mask() == 1
|
||||
assert len(displays) > count
|
||||
for display in displays[count:]:
|
||||
assert display['Tja_D_Stat'] == (7 if hands_free else 2)
|
||||
assert display['FcwVisblWarn_B_Rq'] == 1
|
||||
assert display['FcwAudioWarn_B_Rq'] == 1
|
||||
|
||||
cs.steeringPressed = False
|
||||
cs.steeringAngleDeg = 0.
|
||||
cs.steeringTorque = 0.
|
||||
cs.rightBlinker = False
|
||||
for _ in range(50):
|
||||
step()
|
||||
assert ((steering[-1][0] >> 4) & 7) == 1
|
||||
assert displays[-1]['Tja_D_Stat'] == (7 if hands_free else 2)
|
||||
command.latActive = False
|
||||
for _ in range(25):
|
||||
assert not {address for address, _, _ in step()} & {0x18A, 0x3D8}
|
||||
assert native.safety.safety_fwd_hook(2, 0x18A) == 0
|
||||
assert native.safety.safety_fwd_hook(2, 0x3D8) == 0
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
return steering
|
||||
|
||||
|
||||
@pytest.mark.parametrize('angle,torque,desired,speed,yaw', [
|
||||
(-12.2, -1.6875, .000041086183, 2.55, -.006),
|
||||
(-38.7, -2., -.000324735534, 2.563888788, -.033),
|
||||
])
|
||||
def test_recorded_turn_keeps_dom_display_and_hands_free_does_not_change_steering(angle, torque, desired, speed, yaw):
|
||||
hands_free = run_turn(hands_free=True, angle=angle, torque=torque, desired=desired, speed=speed, yaw=yaw)
|
||||
plain = run_turn(hands_free=False, angle=angle, torque=torque, desired=desired, speed=speed, yaw=yaw)
|
||||
assert plain == hands_free
|
||||
|
||||
|
||||
@pytest.mark.parametrize('saved', [False, True])
|
||||
@pytest.mark.parametrize('flag', [ParamKeyFlag.CLEAR_ON_MANAGER_START, ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION,
|
||||
ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION, ParamKeyFlag.CLEAR_ON_IGNITION_ON])
|
||||
def test_cluster_preference_survives_native_cleanup(tmp_path, saved, flag):
|
||||
params = Params(str(tmp_path))
|
||||
key = 'FordHandsFreeCluster'
|
||||
assert params.check_key(key) and params.get_type(key) == ParamKeyType.BOOL
|
||||
params.put(key, saved, block=True)
|
||||
sentinel = Path(params.get_param_path('FordUnregisteredSentinel'))
|
||||
sentinel.write_bytes(b'1')
|
||||
params.clear_all(flag)
|
||||
assert params.get(key) == saved and not sentinel.exists()
|
||||
reopened = Params(str(tmp_path))
|
||||
assert reopened.get(key) == saved
|
||||
assert Path(reopened.get_param_path(key)).read_bytes() == (b'1' if saved else b'0')
|
||||
assert FordClusterPreference(reopened).enabled == saved
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Mach-E AOL must not inject UI while native safety forwards stock UI."""
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.ford.carcontroller import CarController
|
||||
from opendbc.car.ford.carstate import CarState
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
from opendbc.car.ford.values import CAR, DBC
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
UI = {0x3D8, 0x18A}
|
||||
|
||||
|
||||
def setup(*, aol=True, identity=CAR.FORD_MUSTANG_MACH_E_MK1):
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[0][0x5A] = 8
|
||||
fingerprint[2].update({0x3D6: 8, 0x186: 8})
|
||||
with OpenpilotPrefix():
|
||||
cp = CarInterface.get_params(identity, fingerprint, [], False, False, False)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
# Card finalizes AOL qualification after the controller is constructed.
|
||||
cp.alternativeExperience = 32 if aol else 0
|
||||
state = CarState(cp)
|
||||
state.update(state.get_can_parsers(cp))
|
||||
cs = structs.CarState(canValid=True, vEgo=15., vEgoRaw=15., gearShifter=structs.CarState.GearShifter.drive)
|
||||
cs.cruiseState.available = True
|
||||
state.out = cs.as_reader()
|
||||
return controller, state
|
||||
|
||||
|
||||
def test_mache_idle_leaves_stock_display_and_fcw_untouched_then_owns_active_ui():
|
||||
owner, state = setup()
|
||||
command = structs.CarControl()
|
||||
# Real stock UI/FCW fields remain available to be forwarded by native safety.
|
||||
state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] = 1
|
||||
state.acc_tja_status_stock_values['Tja_D_Stat'] = 1
|
||||
for _ in range(110):
|
||||
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
assert not UI.intersection(address for address, _, _ in frames)
|
||||
command.latActive = True
|
||||
active_ui = set()
|
||||
for _ in range(110):
|
||||
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
active_ui.update(address for address, _, _ in frames if address in UI)
|
||||
assert active_ui == UI
|
||||
command.latActive = False
|
||||
for _ in range(110):
|
||||
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
|
||||
assert not UI.intersection(address for address, _, _ in frames)
|
||||
assert state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity,aol', [(CAR.FORD_MUSTANG_MACH_E_MK1, False), (CAR.FORD_F_150_MK14, True)])
|
||||
def test_other_display_ownership_contracts_keep_existing_idle_ui(identity, aol):
|
||||
owner, state = setup(aol=aol, identity=identity)
|
||||
seen = set()
|
||||
for _ in range(110):
|
||||
_, frames = owner.update(structs.CarControl().as_reader(), state, owner.frame * 10_000_000)
|
||||
seen.update(address for address, _, _ in frames if address in UI)
|
||||
assert seen == UI
|
||||
@@ -28,7 +28,7 @@ class TestNewPortCurvature(unittest.TestCase):
|
||||
for identity in PROFILES:
|
||||
for alpha, release in ((False, False), (True, False), (False, True), (True, True)):
|
||||
cp = factory(identity, alpha, release)
|
||||
expected_long = bool(alpha and (identity == CAR.FORD_EDGE_MK2 or not release))
|
||||
expected_long = bool(alpha)
|
||||
self.assertTrue(cp.pcmCruise)
|
||||
self.assertEqual(cp.openpilotLongitudinalControl, expected_long)
|
||||
self.assertEqual(cp.safetyConfigs[-1].safetyParam, (8 if identity == CAR.FORD_EDGE_MK2 else 10) + int(expected_long))
|
||||
@@ -156,6 +156,7 @@ class TestNewPortCurvature(unittest.TestCase):
|
||||
command, controller_log = controls.state_control()
|
||||
self.assertTrue(command.enabled)
|
||||
control_events = []
|
||||
|
||||
def capture_control(service, event, collector=control_events):
|
||||
if service == "carControl":
|
||||
collector.append(messaging.log_from_bytes(event.to_bytes()))
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.car.card import Car, alpha_long_requested
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.starpilot.controller_extensions import ManualTurnInputs
|
||||
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
||||
@@ -36,7 +36,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
|
||||
saved.put_bool('IsReleaseBranch', release, block=True)
|
||||
saved.put_bool('DisableOpenpilotLongitudinal', disable, block=True)
|
||||
saved.put_bool('FordHumanTurnDetection', False, block=True)
|
||||
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
|
||||
cp = params(identity, alpha, release)
|
||||
cp.carVin = '0' * 17
|
||||
cp.carFw = []
|
||||
expected = cp.to_dict()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Original switch context, state event and reached controller regressions."""
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs, gen_empty_fingerprint
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
@@ -39,6 +41,27 @@ def test_actual_parser_cancel_edges_are_op_long_only_and_contextual():
|
||||
assert cancel == (expected if op_long else [])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('vehicle', [CAR.FORD_ESCAPE_MK4, CAR.FORD_MUSTANG_MACH_E_MK1])
|
||||
@pytest.mark.parametrize('op_long', [False, True])
|
||||
@pytest.mark.parametrize('signal,button', [
|
||||
('CcAslButtnSetIncPress', 'accelCruise'), ('CcAslButtnResIncPress', 'accelCruise'),
|
||||
('CcAslButtnIndxIncPress', 'accelCruise'), ('CcAslButtnSetDecPress', 'decelCruise'),
|
||||
('CcAslButtnResDecPress', 'decelCruise'), ('CcAslButtnIndxDecPress', 'decelCruise'),
|
||||
])
|
||||
def test_directional_cruise_edges_reach_software_cruise_only_with_op_long(vehicle, op_long, signal, button):
|
||||
cp = params(vehicle, alpha=op_long)
|
||||
state = CarState(cp)
|
||||
parsers = state.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for i, (pressed, expected) in enumerate(((0, []), (1, [True]), (1, []), (0, [False]))):
|
||||
parsers[Bus.pt].update([(1_000_000_000 + i * 10_000_000, [
|
||||
packer.make_can_msg('Steering_Data_FD1', parsers[Bus.pt].bus, {signal: pressed}),
|
||||
])])
|
||||
out = state.update(parsers)
|
||||
edges = [event.pressed for event in out.buttonEvents if event.type == getattr(structs.CarState.ButtonEvent.Type, button)]
|
||||
assert edges == (expected if op_long else [])
|
||||
|
||||
|
||||
def test_reached_controller_preserves_precedence_cadence_and_stock_bits():
|
||||
cp = params(CAR.FORD_MUSTANG_MACH_E_MK1, alpha=False)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestFordThreePorts(unittest.TestCase):
|
||||
self.assertTrue(requested.openpilotLongitudinalControl)
|
||||
self.assertTrue(requested.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
|
||||
if candidate == CAR.FORD_MONDEO_MK5:
|
||||
self.assertFalse(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
|
||||
self.assertTrue(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
|
||||
self.assertAlmostEqual(params(CAR.FORD_MUSTANG_MACH_E_MK1).steerActuatorDelay, 0.22, places=7)
|
||||
self.assertFalse(params(CAR.FORD_BRONCO_SPORT_MK1).openpilotLongitudinalControl)
|
||||
self.assertTrue(params(CAR.FORD_BRONCO_SPORT_MK1, alpha=True).openpilotLongitudinalControl)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Transit CAN angle quantization must stay within the native encoded envelope."""
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.ford.fordcan import CanBus, create_lka_msg
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent as _Native
|
||||
|
||||
|
||||
@pytest.mark.parametrize('word', (12, 13))
|
||||
@pytest.mark.parametrize('sign,endpoint', ((1, 4072), (-1, 24)))
|
||||
def test_saturated_angle_with_physical_cruise_stays_inside_native_envelope(sign, endpoint, word):
|
||||
native = _Native(methodName='runTest')
|
||||
native.setUp()
|
||||
try:
|
||||
native.reset(word)
|
||||
native.pump(40, main=3)
|
||||
native.pump(10, main=4, mask=1)
|
||||
assert native.safety.get_controls_allowed()
|
||||
for angle in (1., 2., 3., 4., 5., 5.8, 6.):
|
||||
native.pump(3, main=4, mask=1)
|
||||
address, data, bus = create_lka_msg(native.packer, CanBus(fingerprint=gen_empty_fingerprint()), True, sign * angle,
|
||||
2 if sign > 0 else 4, 0., transit=True)
|
||||
raw_angle = ((data[2] & 15) << 8) | data[3]
|
||||
assert 24 <= raw_angle <= 4072
|
||||
if angle >= 5.8:
|
||||
assert raw_angle == endpoint
|
||||
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
# The adjacent encoded values remain prohibited by unchanged native safety.
|
||||
rejected_angle = 4073 if sign > 0 else 23
|
||||
rejected = bytearray(data)
|
||||
rejected[2] = (rejected[2] & 0xF0) | (rejected_angle >> 8)
|
||||
rejected[3] = rejected_angle & 0xFF
|
||||
assert not native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, rejected))
|
||||
address, data, bus = create_lka_msg(native.packer, CanBus(fingerprint=gen_empty_fingerprint()), False, sign * 6., transit=True)
|
||||
assert data[0] >> 5 == 0
|
||||
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
finally:
|
||||
native.tearDown()
|
||||
@@ -48,6 +48,7 @@ class FordSafetyFlags(IntFlag):
|
||||
MACH_E_EXTENDED = 16
|
||||
CLASSIC_EXTENDED = 32
|
||||
EXPLORER_EXTENDED = CLASSIC_EXTENDED # Existing public profile spelling.
|
||||
MANUAL = 256
|
||||
BLUEPILOT_ANGLE = 128
|
||||
GENERIC_CANFD_EXTENDED = 64
|
||||
|
||||
@@ -328,12 +329,12 @@ FW_QUERY_CONFIG = FwQueryConfig(
|
||||
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
|
||||
logging=True,
|
||||
),
|
||||
Request(
|
||||
*[Request(
|
||||
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
|
||||
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
|
||||
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
|
||||
bus=0,
|
||||
),
|
||||
bus=bus,
|
||||
) for bus in (0, 4)],
|
||||
*[Request(
|
||||
[StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)],
|
||||
[StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Independent lateral axes for exact source-qualified Bolt and Volt owners."""
|
||||
|
||||
import math
|
||||
from functools import lru_cache
|
||||
|
||||
from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader
|
||||
from opendbc.car.gm.cancel_remap import aol_enabled, valid_alternative_experience
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
@@ -35,11 +38,11 @@ GM_AOL_WORDS = (frozenset(range(0xE600, 0xE608)) | frozenset(range(0xE610, 0xE61
|
||||
0xC1D1, 0xC1D3,
|
||||
0xC110, 0xC111, 0xC120, 0xC121, 0xC130, 0xC131, 0xC140, 0xC141,
|
||||
0xE800, 0xE801, 0xE802, 0xE803, 0xE804, 0xE805,
|
||||
0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187,
|
||||
0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC163, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187,
|
||||
))
|
||||
|
||||
|
||||
def qualified_gm(cp) -> bool:
|
||||
def _qualified_gm_uncached(cp) -> bool:
|
||||
try:
|
||||
if (cp.brand != 'gm' or cp.notCar or cp.passive or cp.dashcamOnly or
|
||||
cp.steerControlType != CarParams.SteerControlType.torque or
|
||||
@@ -94,6 +97,30 @@ def qualified_gm(cp) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Qualification depends only on the exact finalized CarParams value. Cache no
|
||||
# live vehicle state, native grant, source timestamp, or output eligibility.
|
||||
@lru_cache(maxsize=8)
|
||||
def _qualified_gm_bytes(key: bytes) -> bool:
|
||||
with CarParams.from_bytes(key) as cp:
|
||||
return _qualified_gm_uncached(cp)
|
||||
|
||||
|
||||
def qualified_gm(cp) -> bool:
|
||||
cp_type = type(cp)
|
||||
if cp_type is _DynamicStructBuilder or cp_type is _DynamicStructReader:
|
||||
try:
|
||||
if cp.schema == CarParams.schema:
|
||||
if cp.brand != 'gm':
|
||||
return False
|
||||
snapshot = cp.copy() if cp_type is _DynamicStructBuilder else cp.as_builder()
|
||||
key = snapshot.to_bytes()
|
||||
if len(key) <= 64 * 1024:
|
||||
return _qualified_gm_bytes(key)
|
||||
except Exception:
|
||||
pass
|
||||
return _qualified_gm_uncached(cp)
|
||||
|
||||
|
||||
def lateral_request(cp, cc) -> bool:
|
||||
"""Only a finalized independent-axis owner may relax its cruise-active lateral gate."""
|
||||
return (qualified_gm(cp) and aol_enabled(cp) and cc.latActive)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Mode-transition output shaping for admitted Bolt longitudinal owners."""
|
||||
"""Mode-transition output shaping for admitted GM longitudinal owners."""
|
||||
|
||||
|
||||
class BoltModeTransition:
|
||||
@@ -37,7 +37,7 @@ class BoltModeTransition:
|
||||
if output < 0.0 and output < last_output:
|
||||
progress = min(1.0, self.timer / self.duration)
|
||||
urgency = abs(output / -4.0)
|
||||
urgency_smooth = min(1.0, urgency ** 0.4)
|
||||
urgency_smooth = min(1.0, urgency**0.4)
|
||||
blend = 1.0 - (1.0 - progress) * (1.0 - urgency_smooth)
|
||||
output = last_output + (output - last_output) * blend
|
||||
return output
|
||||
@@ -45,16 +45,32 @@ class BoltModeTransition:
|
||||
|
||||
def policy_for(cp):
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.gm.values import (CAR, GMFlags, GMSafetyFlags, is_bolt_cc_profile, is_bolt_euv_longitudinal,
|
||||
is_ordinary_cc_profile, is_ordinary_camera_profile, is_conventional_cc_pedal_profile)
|
||||
from opendbc.car.gm.values import (
|
||||
CAR,
|
||||
GMFlags,
|
||||
GMSafetyFlags,
|
||||
is_bolt_cc_profile,
|
||||
is_bolt_euv_longitudinal,
|
||||
is_ordinary_cc_profile,
|
||||
is_ordinary_camera_profile,
|
||||
is_conventional_cc_pedal_profile,
|
||||
is_volt_longitudinal,
|
||||
)
|
||||
from opendbc.car.gm.longitudinal import policy_for as pedal_policy_for
|
||||
|
||||
if not cp.openpilotLongitudinalControl:
|
||||
return None
|
||||
if (is_conventional_cc_pedal_profile(cp) or is_ordinary_camera_profile(cp, longitudinal=True) or
|
||||
is_ordinary_cc_profile(cp) or is_bolt_cc_profile(cp) or is_bolt_euv_longitudinal(cp)):
|
||||
if (
|
||||
is_volt_longitudinal(cp)
|
||||
or is_conventional_cc_pedal_profile(cp)
|
||||
or is_ordinary_camera_profile(cp, longitudinal=True)
|
||||
or is_ordinary_cc_profile(cp)
|
||||
or is_bolt_cc_profile(cp)
|
||||
or is_bolt_euv_longitudinal(cp)
|
||||
):
|
||||
return BoltModeTransition()
|
||||
from opendbc.car.gm.values import gm_control_word
|
||||
|
||||
base = GMSafetyFlags.HW_CAM | GMSafetyFlags.EV | GMSafetyFlags.PEDAL_LONG | GMSafetyFlags.PADDLE_SCHED
|
||||
words = {
|
||||
CAR.CHEVROLET_BOLT_CC_2017: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017,
|
||||
@@ -62,9 +78,12 @@ def policy_for(cp):
|
||||
CAR.CHEVROLET_BOLT_CC_2022_2023: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_GEN2,
|
||||
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL: base | GMSafetyFlags.BOLT_ACC_PEDAL | GMSafetyFlags.BOLT_GEN2,
|
||||
}
|
||||
if (pedal_policy_for(cp) is not None and not cp.flags & GMFlags.CC_LONG.value and
|
||||
len(cp.safetyConfigs) == 1 and
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and
|
||||
gm_control_word(cp) == words.get(cp.carFingerprint)):
|
||||
if (
|
||||
pedal_policy_for(cp) is not None
|
||||
and not cp.flags & GMFlags.CC_LONG.value
|
||||
and len(cp.safetyConfigs) == 1
|
||||
and cp.networkLocation == CarParams.NetworkLocation.fwdCamera
|
||||
and gm_control_word(cp) == words.get(cp.carFingerprint)
|
||||
):
|
||||
return BoltModeTransition()
|
||||
return None
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from opendbc.car.gm.one_pedal import VoltOnePedal
|
||||
from opendbc.car.gm.steering_authority import EPS_STATUS_TIMEOUT_NS
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal, camera_acc_pedal_profile, volt_cc_pedal_profile
|
||||
from opendbc.car.gm.longitudinal import volt_sng_release
|
||||
from opendbc.car.gm.auto_hold import AutoHold, config_for as auto_hold_config_for, hold_brake as estimate_hold_brake
|
||||
@@ -575,7 +576,15 @@ class CarController(CarControllerBase):
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
stock_steer_ready = (stock_steer_ready and CS.pedal_sensor_healthy and
|
||||
0 < CS.pedal_sensor_ts_nanos <= now_nanos <= CS.pedal_sensor_ts_nanos + PEDAL_SENSOR_TIMEOUT_NS)
|
||||
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched
|
||||
# An SDGM EPS can report inactive while the driver holds the brake. Emit
|
||||
# neutral for that measured pause so releasing the brake starts a new request.
|
||||
# Keep the existing acquisition deadline and genuine refusal latch intact.
|
||||
sdgm_brake_pause = False
|
||||
if self.volt_sdgm_long and CS.out.brakePressed:
|
||||
steering = CS.steering_authority
|
||||
sdgm_brake_pause = (steering.enabled and steering.status == 0 and
|
||||
0 < steering.status_ns <= now_nanos <= steering.status_ns + EPS_STATUS_TIMEOUT_NS)
|
||||
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched and not sdgm_brake_pause
|
||||
|
||||
# Steering (Active: 50Hz, inactive: 10Hz)
|
||||
steer_step = self.params.STEER_STEP if lat_active else self.params.INACTIVE_STEER_STEP
|
||||
@@ -1200,7 +1209,7 @@ class CarController(CarControllerBase):
|
||||
self.bolt_removed_cancel_credit_used = CS.conventional_cancel_credit.credit_ns
|
||||
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, CruiseButtons.CANCEL))
|
||||
|
||||
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.bolt_cc_profile:
|
||||
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.bolt_cc_profile and not self.volt_camera_removed:
|
||||
# Silence "Take Steering" alert sent by camera, forward PSCMStatus with HandsOffSWlDetectionStatus=1
|
||||
if self.frame % 10 == 0:
|
||||
can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status))
|
||||
|
||||
@@ -17,7 +17,8 @@ from opendbc.car.gm.conventional_pedal import CancelCredit
|
||||
from opendbc.car.gm.values import (DBC, AccState, CruiseButtons, STEER_THRESHOLD, SDGM_CAR, ALT_ACCS,
|
||||
ASCM_INTERCEPT_CAR, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, GMFlags, GMSafetyFlags, NO_ACC_BOLT_CAR,
|
||||
CC_GATEWAY_STOCK_CAR, requires_camera_state_sources, is_conventional_cc_pedal_profile, is_silverado_cc_pedal_profile,
|
||||
is_volt_cc_profile, is_silverado_cc_stock_profile, is_ordinary_cc_profile, is_malibu_cc_f1_profile, VOLT_BSM_CAR, CAR,
|
||||
is_volt_cc_profile, is_volt_cc_f1_profile, is_silverado_cc_stock_profile, is_ordinary_cc_profile,
|
||||
is_malibu_cc_f1_profile, VOLT_BSM_CAR, CAR,
|
||||
is_volt_gateway_profile, is_volt_gateway_alternate_brake, is_bolt_cc_profile, BOLT_CC_WORDS,
|
||||
is_bolt_pedal_profile, is_bolt_pedal_removed_profile, is_bolt_present_no_acc_pedal_profile, is_volt_camera_removed,
|
||||
is_ordinary_camera_profile, is_ordinary_camera_removed)
|
||||
@@ -268,7 +269,7 @@ class CarState(CarStateBase):
|
||||
pt_cp.ts_nanos["ECMCruiseControl"]["CruiseActive"],
|
||||
pt_cp.ts_nanos["ASCMSteeringButton"]["RollingCounter"],
|
||||
pt_cp.ts_nanos["ECMEngineStatus"]["CruiseMainOn"],
|
||||
(pt_cp.ts_nanos["EBCMBrakePedalPosition"]["BrakePedalPosition"] if is_malibu_cc_f1_profile(self.CP) else
|
||||
(pt_cp.ts_nanos["EBCMBrakePedalPosition"]["BrakePedalPosition"] if is_malibu_cc_f1_profile(self.CP) or is_volt_cc_f1_profile(self.CP) else
|
||||
pt_cp.ts_nanos["ECMAcceleratorPos"]["BrakePedalPos"]),
|
||||
pt_cp.ts_nanos["ECMPRDNL2"]["PRNDL2"],
|
||||
pt_cp.ts_nanos["AcceleratorPedal2"]["AcceleratorPedal2"])
|
||||
@@ -405,7 +406,10 @@ class CarState(CarStateBase):
|
||||
source_be_brake = (gm_control_word(self.CP) &
|
||||
(GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM).value and
|
||||
not gm_control_word(self.CP) & GMSafetyFlags.BRAKE_C9.value)
|
||||
if is_malibu_cc_f1_profile(self.CP):
|
||||
if is_volt_cc_f1_profile(self.CP):
|
||||
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 6 or
|
||||
pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0)
|
||||
elif is_malibu_cc_f1_profile(self.CP):
|
||||
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 21 or
|
||||
pt_cp.vl["EBCMBrakePedalPosition"]["BrakePressed"] != 0 or
|
||||
pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0)
|
||||
@@ -510,6 +514,9 @@ class CarState(CarStateBase):
|
||||
pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
|
||||
|
||||
ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
|
||||
if is_volt_camera_removed(self.CP):
|
||||
# A missing camera can leave the ECU faulted; that is not engaged cruise.
|
||||
ret.cruiseState.enabled &= pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.FAULTED
|
||||
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
|
||||
if (self.CP.carFingerprint in CC_GATEWAY_STOCK_CAR or self.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC):
|
||||
# AcceleratorPedal2 CruiseState is an ACC state, not a conventional-cruise fault source.
|
||||
@@ -761,6 +768,9 @@ class CarState(CarStateBase):
|
||||
|
||||
if CP.carFingerprint == CAR.CHEVROLET_VOLT_CC:
|
||||
pt_messages.append(("EBCMRegenPaddle", 50))
|
||||
if is_volt_cc_f1_profile(CP):
|
||||
pt_messages = [(name, frequency) for name, frequency in pt_messages if name != "ECMAcceleratorPos"]
|
||||
pt_messages.append(("EBCMBrakePedalPosition", 100))
|
||||
|
||||
if is_bolt_cc_profile(CP):
|
||||
pt_messages += [("PSCMStatus", 10), ("ECMCruiseControl", 10), ("ASCMSteeringButton", 33),
|
||||
|
||||
@@ -20,4 +20,7 @@ def automatic_volt_candidate(candidate, fingerprint, candidates):
|
||||
any(pt.get(address) != size for address, size in required.items()) or
|
||||
any(address in pt for address in (0xBE, 0x201, 0x2FF, 0x320))):
|
||||
return candidate
|
||||
return CAR.CHEVROLET_VOLT_CAMERA
|
||||
# The recorded car has ordinary cruise, no ACC and no camera. The absent
|
||||
# camera is not evidence of an ACC actuator owner. Read actual ECM cruise
|
||||
# activity and retain button-only control; explicit ACC selections are above.
|
||||
return CAR.CHEVROLET_VOLT_CC
|
||||
|
||||
@@ -16,7 +16,7 @@ from opendbc.car.gm.values import (volt_cc_pedal_profile, CAR, CarControllerPara
|
||||
ORDINARY_CC_CAR, ORDINARY_CC_WORD, SILVERADO_CC_PEDAL_WORDS,
|
||||
is_silverado_cc_pedal_profile, is_conventional_cc_pedal_profile,
|
||||
CAMERA_STOCK_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CAMERA_ALPHA_CAR, camera_acc_pedal_profile,
|
||||
VOLT_BSM_CAR, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS,
|
||||
VOLT_BSM_CAR, VOLT_CC_F1_WORD, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS,
|
||||
is_bolt_pedal_removed_profile, is_bolt_present_no_acc_pedal_profile)
|
||||
from opendbc.car.gm.values import (malibu_hybrid_profile, MALIBU_HYBRID_SOURCES,
|
||||
is_volt_sdgm_accepted_envelope, volt_sdgm_accepted_accel_max)
|
||||
@@ -229,14 +229,23 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
if candidate == CAR.CHEVROLET_VOLT_CC:
|
||||
# Require the complete source layout for either automatic or manual identity.
|
||||
required = {0xbe: 6, 0x3d1: 8, 0xc9: 8, 0x1e1: 7, 0x1f5: 8, 0x34a: 5, 0x1c4: 8, 0xbd: 7}
|
||||
admitted = not is_release and all(fingerprint[CanBus.POWERTRAIN].get(addr) == size for addr, size in required.items())
|
||||
pt = fingerprint[CanBus.POWERTRAIN]
|
||||
f1 = 0xBE not in pt and pt.get(0xF1) == 6
|
||||
required = {0xF1 if f1 else 0xBE: 6, 0x3d1: 8, 0xc9: 8, 0x1e1: 7, 0x1f5: 8, 0x34a: 5, 0x1c4: 8, 0xbd: 7}
|
||||
if f1:
|
||||
# Admit the recorded no-ACC source graph, never a broken pedal/camera graph.
|
||||
required.update({0x184: 8, 0xD3: 2, 0x130: 1, 0x17D: 6, 0x210: 4, 0x236: 5})
|
||||
f1_layout = (not any(address in pt for address in (0x201, 0x2FF, 0x320)) and
|
||||
not any(address in fingerprint[CanBus.CAMERA] for address in (0x180, 0x320, 0x370)) and
|
||||
not fingerprint[CanBus.OBSTACLE])
|
||||
admitted = (not is_release and (not f1 or f1_layout) and
|
||||
all(fingerprint[CanBus.POWERTRAIN].get(addr) == size for addr, size in required.items()))
|
||||
ret.networkLocation = NetworkLocation.gateway
|
||||
ret.radarUnavailable = True
|
||||
ret.alphaLongitudinalAvailable = False
|
||||
ret.openpilotLongitudinalControl = admitted
|
||||
ret.pcmCruise = not admitted
|
||||
ret.flags = GMFlags.CC_LONG.value if admitted else 0
|
||||
ret.flags = int(GMFlags.CC_LONG | (GMFlags.NO_ACCELERATOR_POS_MSG if f1 else 0)) if admitted else 0
|
||||
ret.minEnableSpeed = 24 * CV.MPH_TO_MS
|
||||
ret.minSteerSpeed = 7 * CV.MPH_TO_MS
|
||||
ret.longitudinalActuatorDelay = 1.0
|
||||
@@ -245,7 +254,7 @@ class CarInterface(CarInterfaceBase):
|
||||
if not admitted:
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
|
||||
else:
|
||||
ret.safetyConfigs[0].safetyParam = int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)
|
||||
ret.safetyConfigs[0].safetyParam = VOLT_CC_F1_WORD if f1 else int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)
|
||||
|
||||
elif candidate in CC_GATEWAY_STOCK_CAR:
|
||||
# Start with stock PCM ownership; qualified button-control owners override below.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.values import GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal
|
||||
from opendbc.car.gm.values import GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal, is_volt_sdgm_accepted_envelope
|
||||
|
||||
# PSCMStatus is expected at 10 Hz. Three periods bound first acquisition and missing feedback.
|
||||
EPS_STATUS_TIMEOUT_NS = 300_000_000
|
||||
@@ -8,7 +8,8 @@ ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
def monitored_profile(CP: structs.CarParams) -> bool:
|
||||
return (is_volt_ascm_longitudinal(CP) and camera_acc_pedal_profile(CP) is None and
|
||||
return ((is_volt_ascm_longitudinal(CP) or is_volt_sdgm_accepted_envelope(CP)) and
|
||||
camera_acc_pedal_profile(CP) is None and
|
||||
not CP.flags & GMFlags.PEDAL_LONG)
|
||||
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ def fixture(identity, removed=False, metric=False, alpha=False, present=False):
|
||||
return cp, ci, CANPacker(DBC[identity][Bus.pt])
|
||||
|
||||
|
||||
def feed(ci, packer, now, counter=0, gas=False, active=True, speed=20, stock=15, brake=False, regen=False, camera=True):
|
||||
def feed(ci, packer, now, counter=0, gas=False, active=True, speed=20, stock=15, brake=False, regen=False, camera=True, eps_status=0):
|
||||
vals = {
|
||||
'PSCMStatus': {},
|
||||
'PSCMStatus': {'LKATorqueDeliveredStatus': eps_status},
|
||||
'ECMCruiseControl': {'CruiseActive': int(active), 'CruiseSetSpeed': stock * 3.6},
|
||||
'ECMEngineStatus': {'CruiseMainOn': 1, 'BrakePressed': int(brake)},
|
||||
'AcceleratorPedal2': {'AcceleratorPedal2': 30 if gas else 0},
|
||||
|
||||
@@ -1032,13 +1032,16 @@ class TestBoltPaddleModes(unittest.TestCase):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
with self.subTest(candidate=candidate), OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1'}):
|
||||
settings = Params()
|
||||
settings.put_bool('AlwaysOnLateral', False, block=True)
|
||||
card = TestGmAol.card(params(candidate, pedal=True, camera=True), settings)
|
||||
ci, cp = card.CI, card.CP
|
||||
self.assertIsNotNone(ci.CC.maneuver_paddle_input)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
safety = libsafety_py.libsafety
|
||||
self.assertEqual(safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam), 0)
|
||||
# This ordinary pedal test must not inherit the previous test's AOL policy.
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(cp.alternativeExperience)
|
||||
self.assertEqual(safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam), 0)
|
||||
recorder = TestGmBoltPedalSafety()
|
||||
recorder.safety = safety
|
||||
control = structs.CarControl()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""SDGM route26: reacquire EPS assistance after a measured brake pause."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from opendbc.car.gm.values import DBC
|
||||
|
||||
|
||||
def frame(ci, packer, tick, *, brake=False, status=1):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
_, rows = feed_camera(SimpleNamespace(update=lambda _: None), packer, now,
|
||||
counter=tick % 4, brake=brake, active=False, eps_status=status)
|
||||
rows = [row for row in rows if row[0] != 0xBE and (tick % 10 == 0 or row[0] != 0x184)]
|
||||
rows.append(packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 30 if brake else 0}))
|
||||
cs = ci.update([(now, rows)])
|
||||
control = structs.CarControl(enabled=False, latActive=True, longActive=False)
|
||||
control.actuators.torque = .2
|
||||
control.hudControl.setSpeed = 30.
|
||||
_, sends = ci.apply(control.as_reader(), now)
|
||||
steering = [data for address, data, bus in sends if address == 0x180 and bus == 0]
|
||||
return cs, steering
|
||||
|
||||
|
||||
@pytest.mark.parametrize('c9', [False, True])
|
||||
@pytest.mark.parametrize('recovered', [False, True])
|
||||
def test_sdgm_brake_pause_emits_neutral_then_reacquires_or_keeps_refusal(c9, recovered):
|
||||
cp = sdgm_params(brake_c9=c9)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(40):
|
||||
frame(ci, packer, tick)
|
||||
assert ci.CS.steering_authority.seen_active
|
||||
emitted = []
|
||||
for tick in range(40, 140):
|
||||
cs, steering = frame(ci, packer, tick, brake=True, status=0)
|
||||
emitted += steering
|
||||
assert cs.brakePressed
|
||||
assert not ci.CS.steering_authority.latched
|
||||
assert emitted and all(not (data[0] & 8) and data[:2] in (b'\x00\x00', b'\x10\x00', b'\x20\x00', b'\x30\x00') for data in emitted)
|
||||
emitted = []
|
||||
for tick in range(140, 185):
|
||||
cs, steering = frame(ci, packer, tick, status=1 if recovered and tick >= 150 else 0)
|
||||
emitted += steering
|
||||
assert any(data[0] & 8 for data in emitted)
|
||||
assert ci.CS.steering_authority.latched == (not recovered)
|
||||
assert cs.steerFaultTemporary == (not recovered)
|
||||
if recovered:
|
||||
assert ci.CS.steering_authority.seen_active
|
||||
|
||||
|
||||
@pytest.mark.parametrize('status', [1, 2, 3])
|
||||
def test_sdgm_active_feedback_and_factory_faults_are_not_brake_recovery(status):
|
||||
cp = sdgm_params()
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
emitted = []
|
||||
for tick in range(40):
|
||||
cs, steering = frame(ci, packer, tick, brake=True, status=status)
|
||||
emitted += steering
|
||||
assert any(data[0] & 8 for data in emitted)
|
||||
assert cs.steerFaultTemporary == (status == 2)
|
||||
assert cs.steerFaultPermanent == (status == 3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('alpha', [False, True])
|
||||
@pytest.mark.parametrize('brake', [False, True])
|
||||
def test_sdgm_brake_recovery_does_not_enable_unengaged_stock_control(alpha, brake):
|
||||
cp = sdgm_params(alpha=alpha)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
emitted = []
|
||||
for tick in range(20):
|
||||
_, steering = frame(ci, packer, tick, brake=brake, status=0)
|
||||
if tick >= 10:
|
||||
emitted += steering
|
||||
assert emitted
|
||||
# The stock ACC fixture is disengaged; this recovery never enables it.
|
||||
assert all(bool(data[0] & 8) == (alpha and not brake) for data in emitted)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Recorded SDGM route18: a steering request is not proof of EPS assistance."""
|
||||
import pytest
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests import test_ascm_intercept as ascm_fixture
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from opendbc.car.gm.values import DBC
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c9", [False, True])
|
||||
def test_sdgm_long_refused_steering_is_not_silently_active(c9):
|
||||
cp = sdgm_params(alpha=True, brake_c9=c9)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
case = ascm_fixture.TestAscmIntercept(methodName="runTest")
|
||||
for tick in range(40):
|
||||
cs = case.authority_frame(ci, packer, tick, status=1)
|
||||
for tick in range(40, 81):
|
||||
cs = case.authority_frame(ci, packer, tick, status=0)
|
||||
assert ci.CS.steering_authority.latched
|
||||
assert cs.steerFaultTemporary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alpha", [False, True])
|
||||
@pytest.mark.parametrize("status,active,speed,driver", [(1, True, 8., 0.), (0, False, 8., 0.),
|
||||
(0, True, 0., 0.), (0, True, 8., 3.), (2, True, 8., 0.), (3, True, 8., 0.)])
|
||||
def test_sdgm_monitor_preserves_deliberate_pauses_and_factory_faults(alpha, status, active, speed, driver):
|
||||
cp = sdgm_params(alpha=alpha)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
case = ascm_fixture.TestAscmIntercept(methodName="runTest")
|
||||
for tick in range(60):
|
||||
cs = case.authority_frame(ci, packer, tick, status=status, active=active and alpha, speed=speed, driver=driver)
|
||||
assert not ci.CS.steering_authority.latched
|
||||
assert cs.steerFaultTemporary == (status == 2)
|
||||
assert cs.steerFaultPermanent == (status == 3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c9", [False, True])
|
||||
def test_sdgm_refusal_requires_physical_disable_neutral_and_fresh_rearm(monkeypatch, c9):
|
||||
# Reuse the full original parser/controller physical sequence for this exact factory CP.
|
||||
monkeypatch.setattr(ascm_fixture, "params", lambda *args, **kwargs: sdgm_params(brake_c9=c9, radar=True))
|
||||
ascm_fixture.TestAscmIntercept(methodName="runTest").test_volt_steering_loss_requires_driver_rearm()
|
||||
@@ -91,6 +91,13 @@ class TestVoltAlternateBrake(unittest.TestCase):
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick, expected in enumerate(baseline['tx']):
|
||||
# Keep every frozen vehicle-side byte. Only the removed camera's
|
||||
# obsolete PSCM replacement is intentionally absent in this topology.
|
||||
if is_volt_camera_removed(cp):
|
||||
self.assertIn(name, ('removed-0', 'removed-1'))
|
||||
camera_status = [message for message in expected if message[0] == 0x184 and message[2] == 2]
|
||||
self.assertEqual(len(camera_status), int(tick % 10 == 0))
|
||||
expected = [message for message in expected if not (message[0] == 0x184 and message[2] == 2)]
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
speed = 20. if tick < 40 or tick >= 100 else 0. if tick < 80 else 6.
|
||||
active, brake, gas = tick < 60 or tick >= 90, 60 <= tick < 65, 65 <= tick < 70
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Recorded camera-bypassed Volt discovery must retain distinct existing owners."""
|
||||
"""Recorded no-camera/no-ACC Volt discovery must retain distinct existing owners."""
|
||||
from contextlib import ExitStack
|
||||
from copy import deepcopy
|
||||
import json
|
||||
@@ -33,20 +33,21 @@ def discover(fingerprint):
|
||||
|
||||
@pytest.mark.parametrize('alpha', [False, True])
|
||||
@pytest.mark.parametrize('release', [False, True])
|
||||
def test_recorded_collision_reaches_existing_camera_removed_owner(alpha, release):
|
||||
def test_recorded_collision_reaches_conventional_cruise_owner(alpha, release):
|
||||
fp = recorded_fingerprint()
|
||||
with patch.object(car_helpers, 'automatic_volt_candidate', side_effect=lambda candidate, *_: candidate):
|
||||
old, _ = discover(fp)
|
||||
assert old is None # The legacy gateway and CC pedal signatures both survive.
|
||||
selected, observed = discover(fp)
|
||||
assert selected == CAR.CHEVROLET_VOLT_CAMERA and observed == fp
|
||||
assert selected == CAR.CHEVROLET_VOLT_CC and observed == fp
|
||||
cp = CarInterface.get_params(selected, observed, [], alpha, release, False)
|
||||
assert not cp.dashcamOnly and cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
|
||||
assert cp.flags & GMFlags.VOLT_CAMERA_REMOVED and cp.flags & GMFlags.VOLT_CAMERA_NO_ACCEL_POS
|
||||
assert is_volt_camera_removed(cp)
|
||||
assert cp.openpilotLongitudinalControl == (alpha and not release)
|
||||
assert cp.pcmCruise == (not alpha or release)
|
||||
assert cp.safetyConfigs[0].safetyParam == (0xC151 if alpha and not release else 0xC150)
|
||||
assert cp.dashcamOnly == release and cp.networkLocation == structs.CarParams.NetworkLocation.gateway
|
||||
assert not is_volt_camera_removed(cp)
|
||||
assert cp.openpilotLongitudinalControl == (not release)
|
||||
assert cp.pcmCruise == release
|
||||
assert cp.safetyConfigs[0].safetyParam == (0 if release else 0xC163)
|
||||
if not release:
|
||||
assert cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG
|
||||
|
||||
|
||||
@pytest.mark.parametrize('address,length', [(0xD3, 2), (0x130, 1), (0x17D, 6), (0x210, 4), (0x236, 5),
|
||||
@@ -125,8 +126,10 @@ def test_auto_card_and_controls_use_correct_profile_but_start_with_commands_off(
|
||||
|
||||
fp = recorded_fingerprint()
|
||||
packets = [CanData(address, bytes(size), bus) for bus, messages in fp.items() for address, size in messages.items()]
|
||||
|
||||
def receive(wait_for_one=False):
|
||||
return [packets] if wait_for_one else []
|
||||
|
||||
def identify(*args, **kwargs):
|
||||
return car_helpers.get_car(receive, *args[1:], **kwargs)
|
||||
with OpenpilotPrefix(), ExitStack() as cleanup:
|
||||
@@ -142,11 +145,11 @@ def test_auto_card_and_controls_use_correct_profile_but_start_with_commands_off(
|
||||
card = Car()
|
||||
cleanup.callback(card.vehicle_startup.close)
|
||||
cleanup.callback(card.params._finalizer)
|
||||
assert card.CP.carFingerprint == CAR.CHEVROLET_VOLT_CAMERA
|
||||
assert card.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC
|
||||
assert card.CP.fingerprintSource == structs.CarParams.FingerprintSource.can
|
||||
assert not card.CP.dashcamOnly and not card.CP.passive
|
||||
assert card.CP.safetyConfigs[0].safetyParam == 0xC150
|
||||
assert not card.CP.openpilotLongitudinalControl
|
||||
assert card.CP.safetyConfigs[0].safetyParam == 0xC163
|
||||
assert card.CP.openpilotLongitudinalControl
|
||||
controls = Controls()
|
||||
cleanup.callback(controls.params._finalizer)
|
||||
assert controls.CP.carFingerprint == card.CP.carFingerprint
|
||||
|
||||
@@ -25,8 +25,8 @@ def camera_params(alpha=True, release=False, camera_length=6, radar=False, sascm
|
||||
return CarInterface.get_params(CAR.CHEVROLET_VOLT_CAMERA, fingerprint, [], alpha, release, False)
|
||||
|
||||
|
||||
def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False, regen=False, low=False, active=True):
|
||||
_, messages = feed(ci, packer, now, counter=counter, speed=speed, gas=gas, brake=brake, regen=regen)
|
||||
def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False, regen=False, low=False, active=True, eps_status=0):
|
||||
_, messages = feed(ci, packer, now, counter=counter, speed=speed, gas=gas, brake=brake, regen=regen, eps_status=eps_status)
|
||||
messages = [message for message in messages if message[0] not in (0x1C4, 0x1F5)]
|
||||
messages += [packer.make_can_msg("AcceleratorPedal2", 0, {"CruiseState": 2 if active else 0, "AcceleratorPedal2": 30 if gas else 0}),
|
||||
packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 6 if low else 4}),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Route 11177: a removed camera must never receive forwarded or host traffic."""
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params, feed_removed
|
||||
from opendbc.car.gm.tests.test_volt_camera_control import camera_params, feed_camera
|
||||
from opendbc.car.gm.values import CAR, DBC
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
@pytest.mark.parametrize('alpha', [False, True])
|
||||
@pytest.mark.parametrize('alternate', [False, True])
|
||||
def test_removed_camera_controller_uses_only_vehicle_bus(alpha, alternate):
|
||||
cp = removed_params(alpha=alpha, alternate=alternate)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
sent = []
|
||||
for tick in range(24):
|
||||
now = 1_000_000_000 + tick * 40_000_000
|
||||
cs, _ = feed_removed(ci, packer, now, counter=tick % 4, active=False)
|
||||
assert cs.canValid and not cs.canTimeout
|
||||
cc = structs.CarControl(enabled=False, latActive=False, longActive=False)
|
||||
_, frames = ci.apply(cc.as_reader(), now + 2)
|
||||
assert all(frame[2] == 0 for frame in frames)
|
||||
sent.extend(frames)
|
||||
assert any(frame[0] == 0x180 for frame in sent)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('word', [0xC150, 0xC151])
|
||||
def test_removed_camera_native_has_no_forwarding_or_camera_transmit(word):
|
||||
safety = libsafety_py.libsafety
|
||||
release = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
|
||||
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, word) == 0
|
||||
safety.init_tests()
|
||||
if release and word == 0xC151:
|
||||
assert not safety.safety_config_valid()
|
||||
return
|
||||
for bus in (0, 1, 2):
|
||||
for address in range(0x800):
|
||||
assert safety.safety_fwd_hook(bus, address) == -1
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CAMERA][Bus.pt])
|
||||
frame = packer.make_can_msg('PSCMStatus', 2, {})
|
||||
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('alpha', [False, True])
|
||||
def test_connected_camera_retains_its_status_delivery(alpha):
|
||||
cp = camera_params(alpha=alpha)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
sent = []
|
||||
for tick in range(12):
|
||||
now = 1_000_000_000 + tick * 40_000_000
|
||||
cs, _ = feed_camera(ci, packer, now, counter=tick % 4, active=False)
|
||||
assert cs.canValid and not cs.canTimeout
|
||||
_, frames = ci.apply(structs.CarControl().as_reader(), now + 2)
|
||||
sent.extend(frames)
|
||||
assert any(frame[0] == 0x184 and frame[2] == 2 for frame in sent)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""A missing-camera ACC fault must not masquerade as engaged cruise."""
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_volt_camera_removed import feed_removed, removed_params
|
||||
from opendbc.car.gm.values import DBC
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
@pytest.mark.parametrize('alpha', [False, True])
|
||||
@pytest.mark.parametrize('alternate', [False, True])
|
||||
@pytest.mark.parametrize('state', [0, 1, 2, 3, 4])
|
||||
def test_removed_camera_cruise_state_keeps_real_fault(alpha, alternate, state):
|
||||
cp = removed_params(alpha=alpha, alternate=alternate)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(20):
|
||||
now = 1_000_000_000 + tick * 40_000_000
|
||||
feed_removed(ci, packer, now, counter=tick % 4)
|
||||
status = packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': state})
|
||||
out = ci.update([(now + 2, [status])])
|
||||
assert out.canValid
|
||||
assert out.accFaulted == (state == 3)
|
||||
assert out.cruiseState.enabled == (state in (1, 2, 4))
|
||||
assert out.cruiseState.standstill == (state == 4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('active', [1, 2, 4])
|
||||
def test_stock_removed_camera_fault_withdraws_native_cruise_and_cancel(active):
|
||||
cp = removed_params(alpha=False)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
safety = libsafety_py.libsafety
|
||||
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC150) == 0
|
||||
safety.init_tests()
|
||||
safety.set_timer(1_000_000)
|
||||
|
||||
def rx(name, values):
|
||||
address, data, bus = packer.make_can_msg(name, 0, values)
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
|
||||
def cancel(counter):
|
||||
address, data, bus = gmcan.create_buttons(packer, 0, counter, 6)
|
||||
return safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
|
||||
rx('ECMEngineStatus', {'CruiseMainOn': 1})
|
||||
rx('AcceleratorPedal2', {'CruiseState': active})
|
||||
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 0,
|
||||
'SteeringButtonChecksum': 0xFF})
|
||||
assert safety.get_controls_allowed()
|
||||
assert cancel(0)
|
||||
rx('AcceleratorPedal2', {'CruiseState': 3})
|
||||
assert not safety.get_controls_allowed()
|
||||
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 1,
|
||||
'SteeringButtonChecksum': 0xFF + 0x4EF})
|
||||
assert not cancel(1)
|
||||
# A subsequent physical active state can recover; a fault is not a latch exemption.
|
||||
rx('AcceleratorPedal2', {'CruiseState': active})
|
||||
assert safety.get_controls_allowed()
|
||||
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 2,
|
||||
'SteeringButtonChecksum': 0xFF + 2 * 0x4EF})
|
||||
safety.set_timer(1_040_001)
|
||||
assert cancel(2)
|
||||
@@ -0,0 +1,130 @@
|
||||
"""No-camera/no-ACC Volt: independent F1 brake source, conventional cruise only."""
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_volt_camera_automatic import discover, recorded_fingerprint
|
||||
from opendbc.car.gm.tests.test_bolt_cc import feed
|
||||
from opendbc.car.gm.values import CAR, DBC, is_volt_cc_profile
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def params(release=False):
|
||||
return CarInterface.get_params(CAR.CHEVROLET_VOLT_CC, recorded_fingerprint(), [], False, release, False)
|
||||
|
||||
|
||||
def tick(ci, packer, now, *, counter=0, active=True, gas=False, brake=False, regen=False, analog=0, fault=True):
|
||||
_, frames = feed(SimpleNamespace(update=lambda messages: None), packer, now,
|
||||
counter=counter, active=active, stock=60. / 3.6, gas=gas, brake=brake, regen=regen, speed=20.)
|
||||
frames = [m for m in frames if m[2] == 0 and m[0] not in (0xBE, 0x1C4, 0x1F5)]
|
||||
frames += [packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': analog}),
|
||||
packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 3 if fault else 0, 'AcceleratorPedal2': 30 if gas else 0}),
|
||||
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 4})]
|
||||
return ci.update([(now, frames)]), frames
|
||||
|
||||
|
||||
def test_recorded_noacc_collision_uses_conventional_owner():
|
||||
assert discover(recorded_fingerprint())[0] == CAR.CHEVROLET_VOLT_CC
|
||||
cp = params()
|
||||
assert not cp.dashcamOnly and is_volt_cc_profile(cp)
|
||||
assert cp.networkLocation == structs.CarParams.NetworkLocation.gateway
|
||||
assert cp.safetyConfigs[0].safetyParam == 0xC163
|
||||
assert not cp.alphaLongitudinalAvailable and cp.radarUnavailable
|
||||
|
||||
|
||||
@pytest.mark.parametrize('active', [False, True])
|
||||
@pytest.mark.parametrize('fault', [False, True])
|
||||
def test_actual_f1_parser_uses_ordinary_cruise_despite_unused_acc_field(active, fault):
|
||||
cp = params()
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for i in range(30):
|
||||
cs, _ = tick(ci, packer, 1_000_000_000 + i * 40_000_000, counter=i % 4, active=active, fault=fault)
|
||||
assert cs.canValid and not cs.canTimeout
|
||||
assert cs.cruiseState.enabled == active
|
||||
assert not cs.accFaulted and not cs.cruiseState.standstill
|
||||
assert cs.cruiseState.speed == pytest.approx(60. / 3.6)
|
||||
assert ci.CS.volt_cc_physical.sources_current(2_160_000_000)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('brake,analog,regen', [(False, 0, False), (True, 0, False), (False, 6, False), (False, 0, True)])
|
||||
def test_f1_driver_inputs_are_preserved_host(brake, analog, regen):
|
||||
cp = params()
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for i in range(30):
|
||||
now = 1_000_000_000 + i * 40_000_000
|
||||
cs, frames = tick(ci, packer, now, counter=i % 4, brake=brake, analog=analog, regen=regen)
|
||||
assert cs.brakePressed == (brake or analog >= 6)
|
||||
assert cs.regenBraking == regen
|
||||
|
||||
|
||||
@pytest.mark.parametrize('driver', ['none', 'foot', 'analog', 'paddle', 'gas'])
|
||||
def test_native_f1_stock_permission_and_driver_withdrawal(driver):
|
||||
safety = libsafety_py.libsafety
|
||||
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
|
||||
cp = params()
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC163) == 0
|
||||
safety.init_tests()
|
||||
for i in range(30):
|
||||
now = 1_000_000_000 + i * 40_000_000
|
||||
safety.set_timer(now // 1000)
|
||||
_, frames = tick(ci, packer, now, counter=i % 4, gas=driver == 'gas', brake=driver == 'foot',
|
||||
analog=6 if driver == 'analog' else 0, regen=driver == 'paddle')
|
||||
for address, data, bus in frames:
|
||||
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
if not debug:
|
||||
assert not safety.safety_config_valid()
|
||||
return
|
||||
safety.safety_tick()
|
||||
assert safety.safety_config_valid()
|
||||
assert bool(safety.get_controls_allowed()) == (driver in ('none', 'gas'))
|
||||
for address, length, bus in [(0x315, 5, 0), (0x2CB, 8, 0), (0x200, 6, 0), (0x184, 8, 2)]:
|
||||
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, bytes(length)))
|
||||
for bus in (0, 1, 2):
|
||||
for address in (0x180, 0x184, 0x1E1, 0x315, 0x2CB):
|
||||
assert safety.safety_fwd_hook(bus, address) == -1
|
||||
|
||||
|
||||
def test_release_f1_factory_keeps_existing_volt_cc_denial():
|
||||
cp = params(release=True)
|
||||
assert cp.dashcamOnly and not cp.openpilotLongitudinalControl
|
||||
assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput
|
||||
|
||||
|
||||
def test_native_f1_button_slots_cannot_be_reused_or_survive_stale_sources():
|
||||
safety = libsafety_py.libsafety
|
||||
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
|
||||
cp = params()
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC163) == 0
|
||||
safety.init_tests()
|
||||
for i in range(40):
|
||||
now = 1_000_000_000 + i * 40_000_000
|
||||
safety.set_timer(now // 1000)
|
||||
_, frames = tick(ci, packer, now, counter=i % 4)
|
||||
for address, data, bus in frames:
|
||||
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
ci.CC.frame = 156
|
||||
command = structs.CarControl(enabled=True, latActive=True, longActive=True)
|
||||
command.hudControl.setSpeed = 25.
|
||||
command.actuators.accel = 1.
|
||||
_, frames = ci.apply(command.as_reader(), now + 1)
|
||||
assert all(bus == 0 for _, _, bus in frames)
|
||||
assert not any(address in (0x315, 0x2CB, 0x2CD, 0x200, 0x370) for address, _, _ in frames)
|
||||
buttons = [m for m in frames if m[0] == 0x1E1]
|
||||
assert len(buttons) == 1
|
||||
address, data, bus = buttons[0]
|
||||
packet = libsafety_py.make_CANPacket(address, bus, data)
|
||||
assert bool(safety.safety_tx_hook(packet)) == debug
|
||||
assert not safety.safety_tx_hook(packet)
|
||||
safety.set_timer(now // 1000 + 300_001)
|
||||
assert not safety.safety_tx_hook(packet)
|
||||
safety.set_timer(now // 1000 + 2_000_000)
|
||||
safety.safety_tick()
|
||||
assert not safety.safety_config_valid()
|
||||
@@ -69,7 +69,7 @@ class TestVoltSdgmControl(unittest.TestCase):
|
||||
setup(cp)
|
||||
for tick in range(16):
|
||||
now = 1_000_000_000 + tick * 40_000_000
|
||||
out, sources = feed_camera(ci, packer, now, counter=tick % 4, gas=tick >= 8, low=tick % 2 == 0)
|
||||
out, sources = feed_camera(ci, packer, now, counter=tick % 4, gas=tick >= 8, low=tick % 2 == 0, eps_status=1)
|
||||
self.assertTrue(out.canValid)
|
||||
for source in sources:
|
||||
native("rx", source, now // 1000)
|
||||
|
||||
@@ -827,15 +827,24 @@ STEER_THRESHOLD = 1.0
|
||||
DBC = CAR.create_dbc_map()
|
||||
|
||||
|
||||
VOLT_CC_F1_WORD = 0xC163
|
||||
|
||||
|
||||
def is_volt_cc_f1_profile(cp: CarParams) -> bool:
|
||||
return is_volt_cc_profile(cp) and bool(cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG)
|
||||
|
||||
|
||||
def is_volt_cc_profile(cp: CarParams) -> bool:
|
||||
"""Exact development-only conventional-cruise owner, with either startup speed choice."""
|
||||
try:
|
||||
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_VOLT_CC and
|
||||
cp.networkLocation == CarParams.NetworkLocation.gateway and not cp.alphaLongitudinalAvailable and
|
||||
not cp.pcmCruise and cp.radarUnavailable and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and control_flags(cp) == int(GMFlags.CC_LONG) and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and
|
||||
control_flags(cp) == int(GMFlags.CC_LONG | (GMFlags.NO_ACCELERATOR_POS_MSG if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else 0)) and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
int(cp.safetyConfigs[0].safetyParam) == int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC))
|
||||
int(cp.safetyConfigs[0].safetyParam) ==
|
||||
(VOLT_CC_F1_WORD if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ def qualified(cp):
|
||||
return (cp.carFingerprint == CAR.HONDA_ACCORD_11G and cp.brand == "honda" and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and
|
||||
cp.transmissionType in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) and
|
||||
cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.alternativeExperience == 0 and
|
||||
cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.alternativeExperience in (0, 32) and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hondaBosch and
|
||||
cp.safetyConfigs[0].safetyParam == (80 | int(bool(cp.flags & HondaFlags.BOSCH_ALT_BRAKE))) and
|
||||
not int(cp.flags) & ~(int(CAR.HONDA_ACCORD_11G.config.flags) |
|
||||
@@ -20,6 +20,10 @@ class AccordMvlStock:
|
||||
self.last_lkas_button_frame = 0
|
||||
|
||||
def update(self, packer, can, cp, control, state, frame):
|
||||
# The independent session can outlive the PCM. Its prior control envelope
|
||||
# must not keep camera button substitution alive after physical withdrawal.
|
||||
if cp.alternativeExperience == 32 and (state.out.brakePressed or not state.out.cruiseState.enabled):
|
||||
return []
|
||||
if not control.enabled or frame % 4 or control.cruiseControl.cancel or control.cruiseControl.resume:
|
||||
return []
|
||||
if (self.lkas_button_send_remaining == 0 and state.lkas_hud["LKAS_READY"] and
|
||||
|
||||
@@ -36,7 +36,10 @@ def qualified(cp):
|
||||
cp.openpilotLongitudinalControl and not cp.pcmCruise and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and len(configs) == 1 and
|
||||
configs[0].safetyModel == structs.CarParams.SafetyModel.hondaNidec and
|
||||
configs[0].safetyParam == (36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32))
|
||||
(configs[0].safetyParam == (36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32) or
|
||||
(len(cp.safetyConfigs) == 1 and cp.alternativeExperience == 32 and
|
||||
configs[0].safetyParam == ((36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32) | 512 |
|
||||
(256 if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN else 0)))))
|
||||
|
||||
|
||||
def create_command(packer, gas_amount, counter):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.honda.values import CAR, HondaFlags
|
||||
from opendbc.car.honda.values import CAR, HondaFlags, HondaSafetyFlags
|
||||
|
||||
CLASSIC_BOSCH = frozenset((CAR.HONDA_NBOX_2G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH,
|
||||
CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID, CAR.ACURA_RDX_3G,
|
||||
@@ -26,40 +26,94 @@ NIDEC = frozenset((
|
||||
CAR.ACURA_MDX_3G_MMR,
|
||||
CAR.ACURA_TLX_1G,
|
||||
))
|
||||
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11))
|
||||
NIDEC_WORDS = frozenset((0, 4, 260))
|
||||
CANFD_STOCK = frozenset((CAR.HONDA_CRV_6G, CAR.HONDA_PILOT_4G,
|
||||
CAR.HONDA_PASSPORT_4G, CAR.ACURA_MDX_4G_MMR, CAR.ACURA_TLX_2G_MMR))
|
||||
ALT_RADAR_STOCK = frozenset((CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR,
|
||||
CAR.ACURA_TLX_2G, CAR.ACURA_MDX_4G))
|
||||
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11, 16, 17, 80, 81, 512, 513, 1058, 1059, 1570, 1571, 2082, 2083, 2211))
|
||||
NIDEC_WORDS = frozenset((0, 4, 260, 544, 548, 804))
|
||||
MANUAL_SELECTION_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
|
||||
|
||||
# qualified() runs several times per 100 Hz frame in card and selfdrived, so
|
||||
# everything that depends only on this module is computed once, and its flag
|
||||
# tests use plain ints: IntFlag operators cost microseconds each in Python.
|
||||
_DETECTED = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM |
|
||||
HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID | HondaFlags.GAS_INTERCEPTOR)
|
||||
_ALLOWED_FLAGS = {str(car): int(car.config.flags) | _DETECTED
|
||||
for car in CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}}
|
||||
_BOSCH, _BOSCH_RADARLESS, _BOSCH_ALT_BRAKE = int(HondaFlags.BOSCH), int(HondaFlags.BOSCH_RADARLESS), int(HondaFlags.BOSCH_ALT_BRAKE)
|
||||
_NIDEC, _NIDEC_ALT_SCM_MESSAGES = int(HondaFlags.NIDEC), int(HondaFlags.NIDEC_ALT_SCM_MESSAGES)
|
||||
_UNSUPPORTED = int(HondaFlags.BOSCH_ALT_RADAR)
|
||||
_FIXED = structs.CarParams.FingerprintSource.fixed
|
||||
_TRANSMISSIONS = (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt)
|
||||
_HONDA_NIDEC, _HONDA_BOSCH = structs.CarParams.SafetyModel.hondaNidec, structs.CarParams.SafetyModel.hondaBosch
|
||||
|
||||
|
||||
def qualified(cp, *, marked_only=False):
|
||||
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) != 1 or
|
||||
cp.fingerprintSource == structs.CarParams.FingerprintSource.fixed or
|
||||
cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) or
|
||||
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) not in (1, 2) or
|
||||
(len(cp.safetyConfigs) == 2 and not (cp.carFingerprint in CANFD_STOCK and
|
||||
cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput and cp.safetyConfigs[0].safetyParam == 0)) or
|
||||
(cp.fingerprintSource == _FIXED and cp.carFingerprint not in MANUAL_SELECTION_PORTS) or cp.transmissionType not in _TRANSMISSIONS or
|
||||
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
|
||||
return False
|
||||
safety = cp.safetyConfigs[0]
|
||||
identity = next((candidate for candidate in CLASSIC_BOSCH | NIDEC | RADARLESS if candidate == cp.carFingerprint), None)
|
||||
if identity is None:
|
||||
safety = cp.safetyConfigs[-1]
|
||||
fingerprint = str(cp.carFingerprint)
|
||||
allowed = _ALLOWED_FLAGS.get(fingerprint)
|
||||
if allowed is None:
|
||||
return False
|
||||
detected = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM | HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID)
|
||||
if int(cp.flags) & ~(int(identity.config.flags) | detected):
|
||||
flags = int(cp.flags)
|
||||
if flags & ~allowed:
|
||||
return False
|
||||
flags = HondaFlags(cp.flags)
|
||||
if flags & (HondaFlags.BOSCH_CANFD | HondaFlags.BOSCH_ALT_RADAR):
|
||||
if flags & int(HondaFlags.GAS_INTERCEPTOR) and fingerprint not in NIDEC:
|
||||
return False
|
||||
if cp.carFingerprint in NIDEC and flags & HondaFlags.NIDEC and not flags & HondaFlags.BOSCH:
|
||||
expected = 4 if flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 0
|
||||
if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
|
||||
if fingerprint in ALT_RADAR_STOCK:
|
||||
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
|
||||
if cp.openpilotLongitudinalControl:
|
||||
expected |= int(HondaSafetyFlags.BOSCH_LONG)
|
||||
if cp.alternativeExperience == 32:
|
||||
expected |= int(HondaSafetyFlags.ALT_RADAR_AOL | HondaSafetyFlags.AOL_BOSCH_LONG)
|
||||
if fingerprint == CAR.ACURA_MDX_4G and cp.alternativeExperience == 32:
|
||||
expected |= int(HondaSafetyFlags.STOCK_TJA)
|
||||
return (bool(flags & _BOSCH) and bool(flags & int(HondaFlags.BOSCH_ALT_RADAR)) and
|
||||
not bool(flags & int(HondaFlags.BOSCH_CANFD)) and bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
|
||||
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
|
||||
if flags & _UNSUPPORTED:
|
||||
return False
|
||||
if fingerprint == CAR.HONDA_ACCORD_11G:
|
||||
from opendbc.car.honda.accord_mvl_stock import qualified as qualified_mvl
|
||||
return qualified_mvl(cp)
|
||||
if fingerprint in CANFD_STOCK:
|
||||
expected = 16 | int(bool(flags & _BOSCH_ALT_BRAKE))
|
||||
return (bool(flags & _BOSCH) and bool(flags & int(HondaFlags.BOSCH_CANFD)) and
|
||||
cp.pcmCruise and not cp.openpilotLongitudinalControl and
|
||||
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
|
||||
if flags & int(HondaFlags.BOSCH_CANFD):
|
||||
return False
|
||||
if fingerprint in NIDEC and flags & _NIDEC and not flags & _BOSCH:
|
||||
expected = 4 if flags & _NIDEC_ALT_SCM_MESSAGES else 0
|
||||
pedal = bool(flags & int(HondaFlags.GAS_INTERCEPTOR))
|
||||
if pedal:
|
||||
expected |= int(HondaSafetyFlags.GAS_INTERCEPTOR)
|
||||
if cp.alternativeExperience == 32:
|
||||
expected |= int(HondaSafetyFlags.NIDEC_PEDAL_AOL)
|
||||
if fingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
|
||||
expected |= 256
|
||||
return (cp.pcmCruise and cp.openpilotLongitudinalControl and
|
||||
safety.safetyModel == structs.CarParams.SafetyModel.hondaNidec and safety.safetyParam == expected)
|
||||
if cp.carFingerprint in CLASSIC_BOSCH and flags & HondaFlags.BOSCH and not flags & HondaFlags.BOSCH_RADARLESS:
|
||||
expected = int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE))
|
||||
return (cp.pcmCruise and not cp.openpilotLongitudinalControl and
|
||||
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
|
||||
if cp.carFingerprint in RADARLESS and flags & HondaFlags.BOSCH_RADARLESS:
|
||||
expected = 8 | int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
|
||||
return (bool(cp.pcmCruise) == (not pedal) and cp.openpilotLongitudinalControl and
|
||||
safety.safetyModel == _HONDA_NIDEC and safety.safetyParam == expected)
|
||||
if fingerprint in CLASSIC_BOSCH and flags & _BOSCH and not flags & _BOSCH_RADARLESS:
|
||||
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
|
||||
if cp.openpilotLongitudinalControl:
|
||||
expected |= int(HondaSafetyFlags.BOSCH_LONG)
|
||||
if fingerprint == CAR.ACURA_RDX_3G:
|
||||
expected |= int(HondaSafetyFlags.RDX_HIGH_GAS)
|
||||
if cp.alternativeExperience == 32:
|
||||
expected |= int(HondaSafetyFlags.CLASSIC_LONG_AOL | HondaSafetyFlags.AOL_BOSCH_LONG)
|
||||
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
|
||||
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
|
||||
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
|
||||
if fingerprint in RADARLESS and flags & _BOSCH_RADARLESS:
|
||||
expected = 8 | int(bool(flags & _BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
|
||||
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
|
||||
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_factory_keeps_stock_acc_without_long_handover(alpha, release):
|
||||
assert not qualified(cp)
|
||||
cp = final_cp()
|
||||
cp.alternativeExperience = 32
|
||||
assert not qualified(cp)
|
||||
assert qualified(cp)
|
||||
|
||||
|
||||
def test_real_parser_controller_camera_button_cadence_and_override_exclusion():
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Exact DEBUG alpha owner retains physical LAT intent without restoring LONG."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.radar_interface import RadarInterface
|
||||
from opendbc.car.honda.tests.test_alt_radar_stock_aol_parity import IDENTITIES, factory
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
def test_actual_alpha_factory_card_serialization_and_explicit_off(identity, alt):
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.starpilot.car.honda.aol import native_accepts_cp, policy_for
|
||||
initial = factory(identity, alt=alt, alpha=True)
|
||||
assert initial.openpilotLongitudinalControl and not initial.pcmCruise
|
||||
assert qualified(initial)
|
||||
addition = 1568 if str(identity) == 'ACURA_MDX_4G' else 1056
|
||||
assert policy_for(initial).safety_param_addition == addition
|
||||
base_word = initial.safetyConfigs[0].safetyParam
|
||||
for enabled in (False, True):
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1'}):
|
||||
saved = Params()
|
||||
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
saved.put_bool('AlwaysOnLateral', enabled, block=True)
|
||||
cp = initial.as_reader().as_builder()
|
||||
card = Car(CI=CarInterface(cp), RI=RadarInterface(cp))
|
||||
assert card.aol_qualified == enabled
|
||||
assert card.CP.safetyConfigs[0].safetyParam == base_word | (addition if enabled else 0)
|
||||
assert card.CP.alternativeExperience == (32 if enabled else 0)
|
||||
assert card.CP.openpilotLongitudinalControl and not card.CP.pcmCruise
|
||||
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
|
||||
assert published.to_dict() == card.CP.to_dict()
|
||||
assert native_accepts_cp(card.CP, 20, card.CP.safetyConfigs[0].safetyParam) == enabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
def test_actual_native_set_brake_and_host_axes_do_not_auto_resume_long(identity, alt):
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
|
||||
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case, car_state
|
||||
with OpenpilotPrefix():
|
||||
case = honda_family_case('honda', True)
|
||||
case.family_cp = factory(identity, alt=alt, alpha=True).as_reader()
|
||||
native = TestHondaStockAol(methodName='runTest')
|
||||
try:
|
||||
sd, _, drive, card, receive = case.family_callers(True)
|
||||
word = card.CP.safetyConfigs[0].safetyParam
|
||||
assert word in (1058, 1059, 1570, 1571)
|
||||
native.configure(('alt-alpha', 20, word, 1, card.CI.CC.packer.dbc.name, False, 'GEARBOX_AUTO', 4))
|
||||
native.stream()
|
||||
native.normal_engage() # Actual SET falling edge from checksummed CAN.
|
||||
native.safety.aol_set_host_request(3)
|
||||
if not native.debug:
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
return # RELEASE native refusal is authoritative, never fabricated grant.
|
||||
assert native.safety.aol_get_permission_mask() == 3
|
||||
axis, _, command = drive(2, allowed=True, native_request_long=True,
|
||||
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
|
||||
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
|
||||
native.press_brake()
|
||||
assert not native.safety.get_controls_allowed()
|
||||
native.safety.aol_set_host_request(1)
|
||||
permission = native.safety.aol_get_permission_mask()
|
||||
assert permission == 1
|
||||
state = car_state(brake=True)
|
||||
axis, _, command = drive(3, observed_state=state, allowed=bool(permission & 1), native_request_long=False,
|
||||
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert not axis.longitudinalActive and not command.longActive
|
||||
assert receive(10_030_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=state)
|
||||
native.stream()
|
||||
axis, _, command = drive(4, native_request_long=False, allowed=bool(native.safety.aol_get_permission_mask() & 1),
|
||||
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
|
||||
assert not native.safety.get_controls_allowed()
|
||||
assert axis.lateralActive and command.latActive and not axis.longitudinalActive and not command.longActive
|
||||
native.stream(gear=2)
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
native.stream()
|
||||
assert native.safety.aol_get_permission_mask() == 1
|
||||
native.stream(eps=7)
|
||||
native.stream(eps=0)
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
finally:
|
||||
libsafety_py.libsafety.set_alternative_experience(0)
|
||||
case.doCleanups()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""ALT_RADAR stock AOL keeps actual factory ownership and exact MDX TJA semantics."""
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.values import CAR, HondaSafetyFlags
|
||||
|
||||
IDENTITIES = (CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR, CAR.ACURA_TLX_2G, CAR.ACURA_MDX_4G)
|
||||
|
||||
|
||||
def factory(identity, *, alt=False, release=False, alpha=False):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[1][0x1A3] = 8
|
||||
if alt:
|
||||
fp[1][0x1BE] = 3
|
||||
return CarInterface.get_params(identity, fp, [], alpha, release, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
@pytest.mark.parametrize('release', (False, True))
|
||||
def test_alt_radar_stock_factory_and_native_marking(identity, alt, release):
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
from openpilot.starpilot.car.honda.aol import native_accepts_cp, policy_for
|
||||
cp = factory(identity, alt=alt, release=release)
|
||||
if identity == CAR.ACURA_RDX_3G_MMR and release:
|
||||
assert cp.dashcamOnly and not qualified(cp)
|
||||
return
|
||||
assert qualified(cp)
|
||||
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
|
||||
policy = policy_for(cp)
|
||||
assert policy.explicit_latch and policy.normal_runtime_supported
|
||||
expected = int(HondaSafetyFlags.STOCK_TJA) if identity == CAR.ACURA_MDX_4G else 0
|
||||
assert policy.safety_param_addition == expected
|
||||
base = cp.safetyConfigs[0].safetyParam
|
||||
cp.safetyConfigs[0].safetyParam |= expected
|
||||
cp.alternativeExperience = 32
|
||||
assert qualified(cp, marked_only=True)
|
||||
assert cp.safetyConfigs[0].safetyParam == base | expected
|
||||
assert native_accepts_cp(cp, 20, base | expected)
|
||||
bad = cp.as_reader().as_builder()
|
||||
bad.safetyConfigs[0].safetyParam ^= 512
|
||||
assert not qualified(bad)
|
||||
assert qualified(factory(identity, alt=alt, alpha=True))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
def test_actual_card_controls_and_native_sources_use_final_stock_cp(identity, alt):
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
|
||||
cp = factory(identity, alt=alt)
|
||||
with OpenpilotPrefix():
|
||||
case = honda_family_case('honda', False)
|
||||
case.family_cp = cp.as_reader()
|
||||
native = TestHondaStockAol(methodName='runTest')
|
||||
try:
|
||||
sd, _, drive, card, receive = case.family_callers(False)
|
||||
assert cp.pcmCruise and not card.CP.openpilotLongitudinalControl
|
||||
word = card.CP.safetyConfigs[0].safetyParam
|
||||
native.configure(('alt-radar', 20, word, 1, card.CI.CC.packer.dbc.name, False, 'GEARBOX_AUTO', 4))
|
||||
native.stream()
|
||||
native.normal_engage()
|
||||
native.press_brake()
|
||||
assert not native.safety.get_controls_allowed()
|
||||
permission = native.safety.aol_get_permission_mask()
|
||||
assert permission == 1
|
||||
axis, _, command = drive(2, allowed=bool(permission & 1), native_request_long=False,
|
||||
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert not axis.longitudinalActive and not command.longActive
|
||||
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
||||
native.stream(eps=1)
|
||||
permission = native.safety.aol_get_permission_mask()
|
||||
assert permission == int(identity == CAR.ACURA_MDX_4G)
|
||||
if identity == CAR.ACURA_MDX_4G:
|
||||
# The configured current parser explicitly regards this status as healthy.
|
||||
from opendbc.can import CANPacker
|
||||
packer = CANPacker(card.CI.CC.packer.dbc.name)
|
||||
msg = packer.make_can_msg('STEER_STATUS', 1, {'STEER_STATUS': 1})
|
||||
card.CI.update([])
|
||||
parsed = card.CI.update([(10_030_000_000, [msg])])
|
||||
assert not parsed.steerFaultPermanent and not parsed.steerFaultTemporary
|
||||
axis, _, command = drive(3, allowed=bool(permission & 1), native_request_long=False,
|
||||
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
|
||||
assert bool(axis.lateralActive) == bool(permission & 1)
|
||||
assert not command.longActive
|
||||
native.stream(eps=7)
|
||||
native.stream(eps=0)
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
finally:
|
||||
libsafety_py.libsafety.set_alternative_experience(0)
|
||||
case.doCleanups()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('word', (512, 513))
|
||||
def test_tja_marker_cannot_be_reused_by_other_host_identity(word):
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
for identity in (CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR, CAR.ACURA_TLX_2G):
|
||||
cp = factory(identity)
|
||||
cp.alternativeExperience = 32
|
||||
cp.safetyConfigs[0].safetyParam = word
|
||||
assert not qualified(cp)
|
||||
cp = factory(CAR.ACURA_MDX_4G)
|
||||
cp.alternativeExperience = 32
|
||||
cp.safetyConfigs = [structs.CarParams.SafetyConfig(safetyModel='noOutput'), cp.safetyConfigs[0]]
|
||||
assert not qualified(cp)
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Factory, Card/Controls, real parser and native Honda stock CANFD contracts."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, gen_empty_fingerprint, structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.values import CAR, HondaFlags
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
IDENTITIES = (CAR.HONDA_CRV_6G, CAR.HONDA_PILOT_4G, CAR.HONDA_PASSPORT_4G,
|
||||
CAR.ACURA_MDX_4G_MMR, CAR.ACURA_TLX_2G_MMR)
|
||||
|
||||
|
||||
def factory(identity, *, alt=False, cvt=False, hybrid=False, offset=0):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[offset][0x191 if cvt else 0x1A3] = 8
|
||||
if alt:
|
||||
fp[offset][0x1BE] = 3
|
||||
if hybrid:
|
||||
fp[offset][0x184] = 8
|
||||
return CarInterface.get_params(identity, fp, [], True, False, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
@pytest.mark.parametrize('cvt,hybrid', ((False, False), (True, True)))
|
||||
def test_actual_factory_exact_stock_aol_admission_and_negative_profiles(identity, alt, cvt, hybrid):
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
from openpilot.starpilot.car.honda.aol import policy_for, native_accepts_cp
|
||||
cp = factory(identity, alt=alt, cvt=cvt, hybrid=hybrid)
|
||||
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
|
||||
assert not cp.alphaLongitudinalAvailable
|
||||
assert bool(cp.flags & HondaFlags.HYBRID) == hybrid
|
||||
assert cp.safetyConfigs[0].safetyParam == 16 | int(alt)
|
||||
assert qualified(cp)
|
||||
policy = policy_for(cp)
|
||||
assert policy.explicit_latch and policy.normal_runtime_supported
|
||||
assert policy.alternative_experience_addition == 32 and policy.safety_param_addition == 0
|
||||
cp.alternativeExperience = 32
|
||||
assert qualified(cp, marked_only=True)
|
||||
assert native_accepts_cp(cp, 20, 16 | int(alt))
|
||||
for field, value in (('passive', True), ('dashcamOnly', True), ('notCar', True),
|
||||
('transmissionType', 'manual'),
|
||||
('flags', int(cp.flags) | (1 << 30))):
|
||||
bad = cp.as_reader().as_builder()
|
||||
setattr(bad, field, value)
|
||||
assert not qualified(bad)
|
||||
for word in (18, 19, 80, 81, 82, 0x4010):
|
||||
bad = cp.as_reader().as_builder()
|
||||
bad.safetyConfigs[0].safetyParam = word
|
||||
assert not qualified(bad)
|
||||
assert not native_accepts_cp(bad, 20, word)
|
||||
assert qualified(factory(identity, offset=4))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
@pytest.mark.parametrize('aol', (False, True))
|
||||
@pytest.mark.parametrize('release', (False, True))
|
||||
def test_actual_card_final_cp_controls_startup_preserves_explicit_off(identity, aol, release, monkeypatch):
|
||||
from opendbc.car.honda.tests.test_stock_startup import exercise_startup
|
||||
exercise_startup(identity, True, release, aol, 16, monkeypatch)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', (CAR.HONDA_PILOT_4G_MMR,))
|
||||
def test_distinct_or_new_platform_does_not_inherit_this_owner(identity):
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
cp = factory(identity)
|
||||
assert not qualified(cp)
|
||||
|
||||
|
||||
def frames(ci, *, cruise=0, lkas=0, brake=False, gear=4, eps=0, main=True):
|
||||
intended = {
|
||||
'SCM_FEEDBACK': {'MAIN_ON': int(main)},
|
||||
'SCM_BUTTONS': {'CRUISE_BUTTONS': 0, 'CRUISE_SETTING': lkas},
|
||||
'ENGINE_DATA': {'XMISSION_SPEED': 45.},
|
||||
'WHEEL_SPEEDS': {f'WHEEL_SPEED_{wheel}': 45. for wheel in ('FL', 'FR', 'RL', 'RR')},
|
||||
'POWERTRAIN_DATA': {'ACC_STATUS': cruise, 'BRAKE_PRESSED': int(brake), 'BRAKE_SWITCH': int(brake), 'PEDAL_GAS': 0},
|
||||
'BRAKE_MODULE': {'BRAKE_PRESSED': int(brake)},
|
||||
'STEER_STATUS': {'STEER_STATUS': eps, 'STEER_TORQUE_SENSOR': 0},
|
||||
ci.CS.gearbox_msg: {'GEAR_SHIFTER': gear},
|
||||
'SEATBELT_STATUS': {'SEATBELT_DRIVER_LATCHED': 1, 'SEATBELT_DRIVER_LAMP': 0},
|
||||
}
|
||||
result = []
|
||||
for role, parser in ci.can_parsers.items():
|
||||
packer = CANPacker(parser.dbc.name)
|
||||
# Keep packer counters across frames; a new packer per publication is invalid.
|
||||
packer = ci._canfd_test_packers.setdefault(role, packer)
|
||||
for address in sorted(parser.addresses):
|
||||
name = parser.dbc.addr_to_msg[address].name
|
||||
values = intended.get(name, {}) if role == Bus.pt else {}
|
||||
assert values.keys() <= parser.dbc.name_to_msg[name].sigs.keys()
|
||||
result.append(packer.make_can_msg(name, parser.bus, values))
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
@pytest.mark.parametrize('offset', (0, 4))
|
||||
def test_parsed_native_brake_reverse_temporary_and_real_host_controller_withdrawal(alt, offset, tmp_path):
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
|
||||
safety = libsafety_py.libsafety
|
||||
cp = factory(CAR.HONDA_CRV_6G, alt=alt, offset=offset)
|
||||
silent = None
|
||||
if offset:
|
||||
import shutil
|
||||
library = tmp_path / 'independent-silent-panda.so'
|
||||
shutil.copyfile(libsafety_py.libpath, library)
|
||||
silent = libsafety_py.ffi.dlopen(str(library))
|
||||
silent.init_tests()
|
||||
silent.set_alternative_experience(32)
|
||||
assert silent.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, 0) == 0
|
||||
silent.set_aol_test_heartbeat(True)
|
||||
silent.aol_set_host_request(3)
|
||||
assert silent.aol_get_permission_mask() == 0
|
||||
with OpenpilotPrefix():
|
||||
case = honda_family_case('honda', False)
|
||||
case.family_cp = cp.as_reader()
|
||||
try:
|
||||
sd, controls, drive, card, receive = case.family_callers(False)
|
||||
assert card.CP.alternativeExperience == 32
|
||||
ci = card.CI
|
||||
ci.update([])
|
||||
ci._canfd_test_packers = {}
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(32)
|
||||
assert safety.set_safety_hooks(20, 16 | int(alt)) == 0
|
||||
safety.set_aol_test_heartbeat(True)
|
||||
|
||||
def parse(tick, **kwargs):
|
||||
now = 10_000_000_000 + tick * 10_000_000
|
||||
data = frames(ci, **kwargs)
|
||||
safety.set_timer((now // 1000) & 0xFFFFFFFF)
|
||||
for address, payload, bus in data:
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus - offset, payload))
|
||||
safety.aol_set_host_request(1)
|
||||
return now, ci.update([(now, data)])
|
||||
|
||||
for tick in range(-40, 0):
|
||||
parse(tick)
|
||||
parse(0, cruise=1) # Accepted real PCM rising edge, not fabricated controlsAllowed.
|
||||
assert safety.get_controls_allowed()
|
||||
_, cs = parse(1, cruise=0, brake=True)
|
||||
assert cs.brakePressed and not safety.get_controls_allowed()
|
||||
assert safety.aol_get_permission_mask() == 1
|
||||
# Existing fixture transports exact current native values to actual SD/Controls.
|
||||
# No native permission is fabricated; host session serialization is synthetic.
|
||||
for tick in range(2, 7):
|
||||
now, cs = parse(tick)
|
||||
request, permission = safety.aol_get_request_mask(), safety.aol_get_permission_mask()
|
||||
axis, _, command = drive(tick, observed_state=structs.CarState(**cs.to_dict()),
|
||||
allowed=bool(permission & 1), native_request_lat=bool(request & 1),
|
||||
native_request_long=False, panda_controls_allowed=bool(safety.get_controls_allowed()))
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert not axis.longitudinalActive and not command.longActive
|
||||
assert receive(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'],
|
||||
state=structs.CarState(**cs.to_dict()))
|
||||
# Exercise Card's real initialization and apply caller, with only wire
|
||||
# publication captured. The command is the real Controls publication.
|
||||
card.sm.update_msgs(now / 1e9, [controls.pm.messages['carControl']])
|
||||
outgoing = []
|
||||
with patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \
|
||||
patch('openpilot.selfdrive.car.card.time.monotonic', return_value=now / 1e9), \
|
||||
patch.object(card, 'publish_sendcan', side_effect=lambda sends, valid, _outgoing=outgoing: _outgoing.extend(sends) if valid else None):
|
||||
card.controls_update(cs, command.as_reader())
|
||||
steering = [f for f in outgoing if f[0] == 0xE4]
|
||||
assert len(steering) == 1
|
||||
for address, payload, bus in steering:
|
||||
packet = libsafety_py.make_CANPacket(address, bus - offset, payload)
|
||||
assert safety.safety_tx_hook(packet)
|
||||
if silent is not None:
|
||||
assert not silent.safety_tx_hook(packet)
|
||||
assert silent.aol_get_permission_mask() == 0
|
||||
for tick, kwargs, expected in ((7, {'gear': 2}, 0), (8, {'eps': 6}, 0), (9, {}, 1),
|
||||
(10, {'eps': 7}, 0), (11, {}, 0)):
|
||||
now, cs = parse(tick, **kwargs)
|
||||
permission = safety.aol_get_permission_mask()
|
||||
assert permission == expected
|
||||
axis, _, command = drive(tick, observed_state=structs.CarState(**cs.to_dict()), allowed=bool(permission & 1),
|
||||
native_request_long=False, panda_controls_allowed=bool(safety.get_controls_allowed()))
|
||||
assert not command.longActive and not axis.longitudinalActive
|
||||
if not expected:
|
||||
assert not command.latActive and not axis.lateralActive
|
||||
finally:
|
||||
safety.set_alternative_experience(0)
|
||||
if silent is not None:
|
||||
silent.set_alternative_experience(0)
|
||||
case.doCleanups()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
def test_canfd_does_not_borrow_classic_can2_diagnostic_exception(identity):
|
||||
from openpilot.starpilot.aol.panda_faults import blocking_fault_mask, blocking_fault_list
|
||||
from openpilot.cereal import log
|
||||
cp = factory(identity)
|
||||
cp.alternativeExperience = 32
|
||||
assert blocking_fault_mask(1 << 3, cp)
|
||||
assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2], cp)
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Actual normal startup for every established classic Bosch alpha identity."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from opendbc.car import structs, gen_empty_fingerprint
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.stock_aol import CLASSIC_BOSCH, qualified
|
||||
from opendbc.car.honda.values import CAR, HondaFlags
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', sorted(CLASSIC_BOSCH, key=str))
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
def test_actual_normal_card_sd_controls_startup(identity, alt):
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[1][0x1A3] = 8
|
||||
if alt:
|
||||
fp[1][0x1BE] = 3
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
|
||||
params = Params()
|
||||
params.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
params.put_bool('AlwaysOnLateral', True, block=True)
|
||||
cp = CarInterface.get_params(identity, fp, [], True, False, False)
|
||||
ci = CarInterface(cp)
|
||||
card = Car(CI=ci, RI=ci.RadarInterface(cp))
|
||||
assert card.aol_qualified
|
||||
sd, controls = SelfdriveD(CP=card.CP), Controls()
|
||||
assert sd.aol_replay and controls.aol_replay # Normal settings, no replay override.
|
||||
assert card.CP.alternativeExperience == 32 and qualified(card.CP, marked_only=True)
|
||||
expected_alt = bool(identity.config.flags & HondaFlags.BOSCH_ALT_BRAKE) or (alt and identity in (CAR.HONDA_ACCORD, CAR.ACURA_RDX_3G))
|
||||
assert bool(card.CP.flags & HondaFlags.BOSCH_ALT_BRAKE) == expected_alt
|
||||
expected = 2082 | int(expected_alt) | (128 if identity == CAR.ACURA_RDX_3G else 0)
|
||||
assert card.CP.safetyConfigs[0].safetyParam == expected
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
with structs.CarParams.from_bytes(params.get('CarParams')) as published:
|
||||
assert published.to_dict() == card.CP.to_dict() == controls.CP.to_dict()
|
||||
assert CarInterface(card.CP).CC.bosch_longitudinal is not None and ci.CC.bosch_longitudinal is not None
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from openpilot.starpilot.aol.tests.honda_can import HondaCan
|
||||
safety = libsafety_py.libsafety
|
||||
codec = HondaCan(ci)
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(32)
|
||||
assert safety.set_safety_hooks(20, expected) == 0
|
||||
safety.set_aol_test_heartbeat(True)
|
||||
|
||||
def observe(tick, **values):
|
||||
now = 1_000_000 + tick * 10_000
|
||||
safety.set_timer(now)
|
||||
frames = codec.frames(**values)
|
||||
for address, payload, bus in frames:
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, payload))
|
||||
safety.aol_set_host_request(3)
|
||||
return ci.update([(now * 1000, frames)])
|
||||
|
||||
try:
|
||||
for tick in range(40):
|
||||
observe(tick)
|
||||
assert safety.aol_get_permission_mask() == 0
|
||||
observe(40, cruise_button=3)
|
||||
observe(41)
|
||||
assert safety.get_controls_allowed() and safety.aol_get_permission_mask() == 3
|
||||
observed = observe(42, brake=True)
|
||||
assert observed.brakePressed and not safety.get_controls_allowed()
|
||||
assert safety.aol_get_permission_mask() == 1
|
||||
observe(43, eps=6)
|
||||
assert safety.aol_get_permission_mask() == 0
|
||||
observe(44)
|
||||
assert safety.aol_get_permission_mask() == 1
|
||||
reverse = [value for value, name in ci.CS.shifter_values.items() if name == 'R']
|
||||
assert len(reverse) == 1
|
||||
observed = observe(45, gear=reverse[0])
|
||||
assert observed.gearShifter == structs.CarState.GearShifter.reverse
|
||||
assert safety.aol_get_permission_mask() == 0
|
||||
observe(46)
|
||||
assert safety.aol_get_permission_mask() == 1
|
||||
observe(47, eps=7)
|
||||
observe(48)
|
||||
assert safety.aol_get_permission_mask() == 0
|
||||
finally:
|
||||
safety.set_alternative_experience(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', sorted(CLASSIC_BOSCH, key=str))
|
||||
def test_classic_alpha_rejects_wrong_namespace_manual_and_diagnostic_borrowing(identity):
|
||||
from openpilot.starpilot.car.honda.aol import policy_for
|
||||
from openpilot.starpilot.aol.panda_faults import blocking_fault_mask
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[1][0x1A3] = 8
|
||||
cp = CarInterface.get_params(identity, fp, [], True, False, False)
|
||||
assert qualified(cp)
|
||||
cp.safetyConfigs[0].safetyParam |= policy_for(cp).safety_param_addition
|
||||
cp.alternativeExperience = 32
|
||||
assert qualified(cp, marked_only=True)
|
||||
assert blocking_fault_mask(1 << 3, cp)
|
||||
for word in (34, 35, 163, 1058, 1570, 2080, 2084, 2210):
|
||||
bad = cp.as_reader().as_builder()
|
||||
bad.safetyConfigs[0].safetyParam = word
|
||||
assert not qualified(bad, marked_only=True)
|
||||
cp.transmissionType = 'manual'
|
||||
assert not qualified(cp)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('word,ceiling', ((2082, 2000), (2083, 2000), (2211, 2200)))
|
||||
def test_actual_native_classic_alpha_gas_ceiling_and_brake_withdrawal(word, ceiling):
|
||||
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol, CASES
|
||||
case = TestHondaStockAol()
|
||||
try:
|
||||
case.configure(next(row for row in CASES if row[2] == word))
|
||||
case.stream()
|
||||
case.normal_engage()
|
||||
case.safety.aol_set_host_request(3)
|
||||
assert case.safety.aol_get_permission_mask() == 3
|
||||
for gas, allowed in ((ceiling, True), (ceiling + 1, False)):
|
||||
packet = case.packet('ACC_CONTROL', {'GAS_COMMAND': gas, 'ACCEL_COMMAND': 0, 'BRAKE_REQUEST': 0})
|
||||
assert bool(case.safety.safety_tx_hook(packet)) == allowed
|
||||
case.press_brake()
|
||||
assert case.safety.aol_get_permission_mask() == 1
|
||||
assert not case.safety.safety_tx_hook(case.packet('ACC_CONTROL', {'GAS_COMMAND': 1, 'ACCEL_COMMAND': 0, 'BRAKE_REQUEST': 0}))
|
||||
finally:
|
||||
case.tearDown()
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Real forced-selection factory must not strand established manual-only ports."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can.dbc import DBC as CANDBC
|
||||
from opendbc.car import Bus, car_helpers, gen_empty_fingerprint, structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.values import CAR, DBC
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
IDS = (CAR.HONDA_CRV_SA, CAR.HONDA_CLARITY, CAR.HONDA_ACCORD_9G, CAR.ACURA_MDX_3G, CAR.ACURA_MDX_3G_MMR, CAR.ACURA_TLX_1G, CAR.HONDA_FIT_4G, CAR.ACURA_INTEGRA)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDS)
|
||||
def test_real_registered_manual_selection_reaches_card_controls_without_native_grant(identity):
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
fp = gen_empty_fingerprint()
|
||||
messages = CANDBC(DBC[identity][Bus.pt]).name_to_msg
|
||||
gear = messages.get('GEARBOX_AUTO') or messages.get('GEARBOX_CVT')
|
||||
fp[0][gear.address] = gear.size
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1', 'FINGERPRINT': ''}):
|
||||
saved = Params()
|
||||
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
saved.put_bool('AlwaysOnLateral', True, block=True)
|
||||
# Patch acquisition only. Actual get_car applies forced selection/source=fixed
|
||||
# and runs the real factory/controller. Never relabel fixed as automatic FW.
|
||||
observation = (None, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, False)
|
||||
with patch.object(car_helpers, 'fingerprint', return_value=observation):
|
||||
ci = car_helpers.get_car(lambda *a: [], lambda *a: None, lambda *a: None, False, False, forced_candidate=str(identity))
|
||||
assert ci.CP.fingerprintSource == structs.CarParams.FingerprintSource.fixed
|
||||
assert qualified(ci.CP)
|
||||
card = Car(CI=ci, RI=CarInterface.RadarInterface(ci.CP))
|
||||
assert card.aol_qualified and card.CP.alternativeExperience == 32
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
|
||||
assert published.fingerprintSource == structs.CarParams.FingerprintSource.fixed
|
||||
assert published.to_dict() == card.CP.to_dict()
|
||||
controls = Controls()
|
||||
assert controls.CP.to_dict() == card.CP.to_dict()
|
||||
safety = libsafety_py.libsafety
|
||||
try:
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(32)
|
||||
cfg = card.CP.safetyConfigs[0]
|
||||
assert safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam) == 0
|
||||
safety.set_aol_test_heartbeat(True)
|
||||
safety.aol_set_host_request(1)
|
||||
assert safety.aol_get_permission_mask() == 0 # Selection supplies no physical authority.
|
||||
finally:
|
||||
safety.set_alternative_experience(0)
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Nidec pedal AOL preserves the actual selected sender and current sensor gate."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.radar_interface import RadarInterface
|
||||
from opendbc.car.honda.tests.test_nidec_interceptor import IDENTITIES, factory
|
||||
from opendbc.car.honda.values import CAR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', IDENTITIES)
|
||||
def test_actual_pedal_factory_card_and_reconstructed_controller_keep_the_sender(identity):
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
from opendbc.car.honda.nidec_interceptor import qualified as pedal_qualified
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.starpilot.car.honda.aol import native_accepts_cp
|
||||
original = factory(identity)
|
||||
assert pedal_qualified(original) and qualified(original)
|
||||
base = original.safetyConfigs[0].safetyParam
|
||||
for enabled in (False, True):
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION':'1', 'AOL_REPLAY_RUNTIME':'1'}):
|
||||
saved = Params()
|
||||
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
saved.put_bool('AlwaysOnLateral', enabled, block=True)
|
||||
cp = original.as_reader().as_builder()
|
||||
card = Car(CI=CarInterface(cp), RI=RadarInterface(cp))
|
||||
expected = base | ((512 | (256 if identity == CAR.HONDA_ODYSSEY_TWN else 0)) if enabled else 0)
|
||||
assert card.CP.safetyConfigs[0].safetyParam == expected
|
||||
assert card.CP.alternativeExperience == (32 if enabled else 0)
|
||||
assert card.aol_qualified == enabled
|
||||
assert not card.CP.pcmCruise and card.CP.openpilotLongitudinalControl
|
||||
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
|
||||
assert published.to_dict() == card.CP.to_dict()
|
||||
restored = published.as_builder()
|
||||
# Construct from FINAL published params, not merely the pre-marker object.
|
||||
rebuilt = CarInterface(restored)
|
||||
assert rebuilt.CC.nidec_interceptor is not None
|
||||
assert pedal_qualified(restored)
|
||||
assert native_accepts_cp(restored, 1, expected) == enabled
|
||||
bad = restored.as_reader().as_builder()
|
||||
bad.safetyConfigs[0].safetyParam |= 0x4000
|
||||
assert not qualified(bad) and not pedal_qualified(bad)
|
||||
|
||||
|
||||
CASES = (
|
||||
('pedal', 1, 544, 0, 'honda_civic_touring_2016_can_generated', False, 'GEARBOX_CVT', 4),
|
||||
('pedal-alt', 1, 548, 0, 'acura_ilx_2016_can_generated', True, 'GEARBOX_AUTO', 4),
|
||||
('pedal-twn', 1, 804, 0, 'honda_odyssey_twn_2018_generated', True, 'GEARBOX_AUTO', 4),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', CASES)
|
||||
@pytest.mark.parametrize('loss', ('sensor_fault', 'sensor_expired', 'wrong_bus', 'bad_tx_crc'))
|
||||
def test_actual_pedal_native_brake_sensor_and_transmitted_crc_contract(profile, loss):
|
||||
from opendbc.car.honda.nidec_interceptor import create_command
|
||||
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
native = TestHondaStockAol(methodName='runTest')
|
||||
try:
|
||||
native.configure(profile)
|
||||
native.stream()
|
||||
native.normal_engage()
|
||||
native.safety.aol_set_host_request(3)
|
||||
assert native.safety.get_controls_allowed()
|
||||
assert native.safety.aol_get_permission_mask() == 3
|
||||
active = create_command(native.packer, .5, 0)
|
||||
# CANPackerSafety inherits the actual packer; use its real message encoding.
|
||||
|
||||
def packet(msg):
|
||||
return libsafety_py.make_CANPacket(msg[0], msg[2], msg[1])
|
||||
assert native.safety.safety_tx_hook(packet(active))
|
||||
native.press_brake()
|
||||
assert not native.safety.get_controls_allowed()
|
||||
assert native.safety.aol_get_permission_mask() == 1
|
||||
assert not native.safety.safety_tx_hook(packet(active))
|
||||
neutral = create_command(native.packer, 0., 1)
|
||||
assert native.safety.safety_tx_hook(packet(neutral))
|
||||
native.stream()
|
||||
assert not native.safety.get_controls_allowed() # No longitudinal auto-resume.
|
||||
if loss == 'sensor_fault':
|
||||
assert native.safety.safety_rx_hook(native.packet('GAS_SENSOR', {'STATE':1}))
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
elif loss in ('sensor_expired', 'wrong_bus'):
|
||||
native.safety.set_timer(1_200_001)
|
||||
native.stream(omit='GAS_SENSOR')
|
||||
if loss == 'wrong_bus':
|
||||
assert native.safety.safety_rx_hook(native.packet('GAS_SENSOR', {'STATE':0}, 1))
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
else:
|
||||
data = bytearray(neutral[1])
|
||||
data[5] ^= 1
|
||||
assert not native.safety.safety_tx_hook(libsafety_py.make_CANPacket(neutral[0], neutral[2], data))
|
||||
if loss != 'bad_tx_crc':
|
||||
native.stream()
|
||||
assert native.safety.aol_get_permission_mask() == 0
|
||||
finally:
|
||||
libsafety_py.libsafety.set_alternative_experience(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', (CAR.HONDA_CIVIC, CAR.ACURA_ILX, CAR.HONDA_ODYSSEY_TWN))
|
||||
def test_actual_pedal_card_sd_controls_lat_only_preserves_long_pause(identity):
|
||||
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
|
||||
with OpenpilotPrefix():
|
||||
case = honda_family_case('honda', True)
|
||||
case.family_cp = factory(identity).as_reader()
|
||||
try:
|
||||
sd, _, drive, card, receive = case.family_callers(False)
|
||||
assert card.CI.CC.nidec_interceptor is not None
|
||||
axis, _, command = drive(2, native_request_long=False, panda_controls_allowed=False)
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert not axis.longitudinalActive and not command.longActive
|
||||
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
||||
finally:
|
||||
case.doCleanups()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Exact current selected CP and two-Panda owner, never a selection-made grant."""
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint, structs, Bus, car_helpers
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.hondacan import CanBus
|
||||
from opendbc.car.honda.values import CAR, DBC
|
||||
from opendbc.car.honda.stock_aol import CANFD_STOCK, CLASSIC_BOSCH, NIDEC, RADARLESS, ALT_RADAR_STOCK, qualified
|
||||
from opendbc.car.honda.tests.test_manual_honda_port_aol import IDS
|
||||
from openpilot.starpilot.aol.vehicle import native_matches_cp
|
||||
from openpilot.starpilot.aol.runtime import native_inventory_matches_cp
|
||||
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState
|
||||
from openpilot.starpilot.car.honda.aol import policy_for
|
||||
|
||||
|
||||
EXPECTED_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
|
||||
|
||||
|
||||
MANUAL_SELECTION_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', sorted(EXPECTED_PORTS - set(IDS), key=str))
|
||||
def test_remaining_registered_selection_uses_actual_factory_card_controls_native_zero(identity):
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from opendbc.can.dbc import DBC as CANDBC
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
|
||||
fp = gen_empty_fingerprint()
|
||||
preliminary = CarInterface.get_params(identity, fp, [], False, False, False)
|
||||
messages = CANDBC(DBC[identity][Bus.pt]).name_to_msg
|
||||
gear = messages.get('GEARBOX_AUTO') or messages.get('GEARBOX_CVT')
|
||||
fp[CanBus(preliminary).pt][gear.address] = gear.size
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1', 'FINGERPRINT': ''}):
|
||||
params = Params()
|
||||
params.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
params.put_bool('AlwaysOnLateral', True, block=True)
|
||||
observation = (None, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, False)
|
||||
with patch.object(car_helpers, 'fingerprint', return_value=observation):
|
||||
ci = car_helpers.get_car(lambda *a: [], lambda *a: None, lambda *a: None, False, False, forced_candidate=str(identity))
|
||||
assert ci.CP.fingerprintSource == structs.CarParams.FingerprintSource.fixed
|
||||
assert qualified(ci.CP)
|
||||
assert identity in MANUAL_SELECTION_PORTS
|
||||
card = Car(CI=ci, RI=ci.RadarInterface(ci.CP))
|
||||
assert card.aol_qualified and not card.aol_card_intent.allowed_latch
|
||||
assert Controls().CP.to_dict() == card.CP.to_dict()
|
||||
safety = libsafety_py.libsafety
|
||||
try:
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(32)
|
||||
cfg = card.CP.safetyConfigs[0]
|
||||
assert safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam) == 0
|
||||
safety.set_aol_test_heartbeat(True)
|
||||
safety.aol_set_host_request(1)
|
||||
assert safety.aol_get_permission_mask() == 0
|
||||
finally:
|
||||
safety.set_alternative_experience(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', sorted(CANFD_STOCK, key=str))
|
||||
@pytest.mark.parametrize('alt', (False, True))
|
||||
def test_actual_two_panda_factory_matches_only_active_native_owner_and_complete_inventory(identity, alt):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[4][0x1A3] = 8
|
||||
if alt:
|
||||
fp[4][0x1BE] = 3
|
||||
cp = CarInterface.get_params(identity, fp, [], True, False, False)
|
||||
assert len(cp.safetyConfigs) == 2 and str(cp.safetyConfigs[0].safetyModel) == 'noOutput'
|
||||
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
|
||||
assert qualified(cp)
|
||||
cp.alternativeExperience = policy_for(cp).alternative_experience_addition
|
||||
word = 16 | int(alt)
|
||||
assert qualified(cp, marked_only=True)
|
||||
assert native_matches_cp(cp, 20, word)
|
||||
assert not native_matches_cp(cp, int(cp.safetyConfigs[0].safetyModel.raw), 0)
|
||||
assert CarInterface(cp).CC.CAN.pt == 4
|
||||
slots = (PandaSlot(0, 'silent', int(cp.safetyConfigs[0].safetyModel.raw), 0, 32), PandaSlot(1, 'honda', 20, word, 32))
|
||||
state = SafetyState(1, True, 10_000_000_000, 10_200_000_000, 20, word, False, False, False, False, 'honda', 'session', 10_000_000_000, slots)
|
||||
assert native_inventory_matches_cp(state, cp)
|
||||
assert not state.lateralAllowed and not state.longitudinalAllowed
|
||||
assert not native_inventory_matches_cp(replace(state, pandaInventory=()), cp) # Legacy v1 cannot admit two slots.
|
||||
for bad_slots in (
|
||||
slots[::-1],
|
||||
(slots[0], replace(slots[1], hardwareSerial='silent')),
|
||||
(replace(slots[0], controlsAllowed=True), slots[1]),
|
||||
(slots[0], replace(slots[1], safetyRxChecksInvalid=True)),
|
||||
(slots[0], replace(slots[1], faults=1)),
|
||||
(slots[0], replace(slots[1], heartbeatLost=True)),
|
||||
):
|
||||
assert not native_inventory_matches_cp(replace(state, pandaInventory=bad_slots), cp)
|
||||
bad = cp.as_reader().as_builder()
|
||||
bad.safetyConfigs[0].safetyParam = 1
|
||||
assert not qualified(bad)
|
||||
bad = cp.as_reader().as_builder()
|
||||
bad.safetyConfigs[1].safetyParam = 18
|
||||
assert not qualified(bad)
|
||||
assert not native_matches_cp(bad, 20, 18)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', sorted(CANFD_STOCK, key=str))
|
||||
def test_actual_two_slot_card_publishes_final_cp_and_keeps_initial_intent_unarmed(identity):
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[4][0x1A3] = 8
|
||||
initial = CarInterface.get_params(identity, fp, [], False, False, False)
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1'}):
|
||||
params = Params()
|
||||
params.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
params.put_bool('AlwaysOnLateral', True, block=True)
|
||||
ci = CarInterface(initial)
|
||||
card = Car(CI=ci, RI=ci.RadarInterface(initial))
|
||||
assert card.aol_qualified and card.CP.alternativeExperience == 32
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
with structs.CarParams.from_bytes(params.get('CarParams')) as published:
|
||||
assert published.to_dict() == card.CP.to_dict()
|
||||
assert Controls().CP.to_dict() == card.CP.to_dict()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', sorted(CANFD_STOCK, key=str))
|
||||
def test_actual_two_slot_card_sd_controls_consume_exact_v2_inventory(identity):
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
|
||||
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[4][0x1A3] = 8
|
||||
cp = CarInterface.get_params(identity, fp, [], False, False, False)
|
||||
with OpenpilotPrefix():
|
||||
case = honda_family_case('honda', False)
|
||||
case.family_cp = cp.as_reader()
|
||||
try:
|
||||
sd, _, drive, card, receive = case.family_callers(False)
|
||||
axis, _, command = drive(2, native_request_long=False, panda_controls_allowed=False)
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert not axis.longitudinalActive and not command.longActive
|
||||
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
||||
denied, _, command = drive(3, native_valid=False, native_request_long=False)
|
||||
assert not denied.lateralActive and not command.latActive
|
||||
finally:
|
||||
case.doCleanups()
|
||||
@@ -34,7 +34,7 @@ STARTUP_CASES = (
|
||||
)
|
||||
|
||||
|
||||
def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware=()):
|
||||
def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware=(), *, automatic_firmware=False):
|
||||
from opendbc.car import car_helpers
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
@@ -60,7 +60,9 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
|
||||
initial = CarInterface.get_params(identity, fingerprint, list(firmware), False, False, False)
|
||||
initial.carVin = "0" * 17
|
||||
initial.carFw = list(firmware)
|
||||
if firmware:
|
||||
if automatic_firmware:
|
||||
initial.fingerprintSource = structs.CarParams.FingerprintSource.fw
|
||||
if firmware and not automatic_firmware:
|
||||
assert initial.flags & HondaFlags.EPS_MODIFIED
|
||||
assert not initial.dashcamOnly
|
||||
expected = initial.to_dict()
|
||||
@@ -73,8 +75,18 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
|
||||
try:
|
||||
observed = (identity, fingerprint, initial.carVin, list(firmware), initial.fingerprintSource, True)
|
||||
with monkeypatch.context() as discovery:
|
||||
discovery.setattr(car_helpers, "fingerprint", lambda *args: observed)
|
||||
discovery.setattr(card_module, "can_comm_callbacks", lambda *args: (lambda *args: [], lambda frames: None))
|
||||
if automatic_firmware:
|
||||
discovery.setenv("FINGERPRINT", "")
|
||||
discovery.setenv("SKIP_FW_QUERY", "")
|
||||
discovery.setenv("DISABLE_FW_CACHE", "1")
|
||||
discovery.setattr(car_helpers, "get_vin", lambda *args: (-1, -1, initial.carVin))
|
||||
discovery.setattr(car_helpers, "get_present_ecus", lambda *args: set())
|
||||
discovery.setattr(car_helpers, "can_fingerprint", lambda *args: (None, fingerprint))
|
||||
discovery.setattr(car_helpers, "get_fw_versions_ordered", lambda *args: list(firmware))
|
||||
discovery.setattr(card_module, "obd_callback", lambda *args: lambda enabled: None)
|
||||
else:
|
||||
discovery.setattr(car_helpers, "fingerprint", lambda *args: observed)
|
||||
discovery.setattr(card_module, "can_comm_callbacks", lambda *args: (lambda *args, **kwargs: [], lambda frames: None))
|
||||
initial_can = messaging.new_message("can", 1)
|
||||
discovery.setattr(card_module.messaging, "recv_one_retry", lambda socket: initial_can)
|
||||
card = Car()
|
||||
@@ -98,7 +110,22 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
|
||||
command, lateral_log = controls.state_control()
|
||||
assert not command.enabled and not command.latActive and not command.longActive
|
||||
controls.publish(command, lateral_log)
|
||||
ci.apply(command.as_reader(), 1_000_000_000)
|
||||
_, outgoing = ci.apply(command.as_reader(), 1_000_000_000)
|
||||
if automatic_firmware:
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
safety = libsafety_py.libsafety
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(card.CP.alternativeExperience)
|
||||
try:
|
||||
assert safety.set_safety_hooks(card.CP.safetyConfigs[0].safetyModel.raw,
|
||||
card.CP.safetyConfigs[0].safetyParam) == 0
|
||||
assert not safety.get_controls_allowed()
|
||||
steering = ci.CC.packer.dbc.name_to_msg["STEERING_CONTROL"].address
|
||||
assert any(address == steering and bus == ci.CC.CAN.lkas for address, _, bus in outgoing)
|
||||
for address, data, bus in outgoing:
|
||||
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
finally:
|
||||
safety.set_alternative_experience(0)
|
||||
finally:
|
||||
del controls, card, ci, subscriber
|
||||
gc.collect()
|
||||
@@ -121,3 +148,18 @@ def test_known_modified_eps_final_cp_publication_and_disabled_startup(identity,
|
||||
firmware = (structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.eps, address=0x18DA30F1,
|
||||
subAddress=0, fwVersion=version, brand="honda"),)
|
||||
exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("identity", (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_CIVIC))
|
||||
def test_automatic_fw_final_cp_and_disabled_native_handoff(identity, monkeypatch):
|
||||
"""Acquisition is controlled; real FW matching, Card factory and native TX run."""
|
||||
from opendbc.car.fw_versions import match_fw_to_car
|
||||
from opendbc.car.honda.fingerprints import FW_VERSIONS
|
||||
|
||||
firmware = [structs.CarParams.CarFw(ecu=ecu, address=address, subAddress=sub or 0,
|
||||
fwVersion=versions[0], brand="honda")
|
||||
for (ecu, address, sub), versions in FW_VERSIONS[identity].items()]
|
||||
exact, matches = match_fw_to_car(firmware, "0" * 17, log=False)
|
||||
assert exact and matches == {identity}
|
||||
exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware,
|
||||
automatic_firmware=True)
|
||||
|
||||
@@ -31,7 +31,7 @@ def final_cp(identity, alpha=False, release=False, *, hybrid=False):
|
||||
def test_actual_factory_admission_and_marker(identity, alpha, release, hybrid):
|
||||
cp = final_cp(identity, alpha, release, hybrid=hybrid)
|
||||
assert bool(cp.flags & HondaFlags.HYBRID) == hybrid
|
||||
expected = not (identity in CLASSIC_BOSCH and cp.openpilotLongitudinalControl)
|
||||
expected = True
|
||||
assert qualified(cp) == expected
|
||||
if not expected:
|
||||
return
|
||||
@@ -46,7 +46,12 @@ def test_actual_factory_admission_and_marker(identity, alpha, release, hybrid):
|
||||
assert qualified(cp, marked_only=True)
|
||||
assert policy_for(cp).ordinary_axis_ack_required
|
||||
assert cp.alternativeExperience == 32
|
||||
assert cp.safetyConfigs[0].safetyParam == (260 if identity == CAR.HONDA_ODYSSEY_TWN else before["safetyConfigs"][0]["safetyParam"])
|
||||
expected_word = before["safetyConfigs"][0]["safetyParam"]
|
||||
if identity == CAR.HONDA_ODYSSEY_TWN:
|
||||
expected_word |= 256
|
||||
if identity in CLASSIC_BOSCH and cp.openpilotLongitudinalControl:
|
||||
expected_word |= 2080
|
||||
assert cp.safetyConfigs[0].safetyParam == expected_word
|
||||
assert cp.openpilotLongitudinalControl == before["openpilotLongitudinalControl"]
|
||||
assert cp.pcmCruise == before["pcmCruise"]
|
||||
assert native_accepts_cp(cp, cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam)
|
||||
@@ -60,7 +65,7 @@ def test_actual_factory_admission_and_marker(identity, alpha, release, hybrid):
|
||||
def test_manual_unknown_namespace_and_extra_panda_denied(identity, hybrid):
|
||||
cp = final_cp(identity, hybrid=hybrid)
|
||||
cp.fingerprintSource = structs.CarParams.FingerprintSource.fixed
|
||||
assert not qualified(cp)
|
||||
assert qualified(cp) # Explicit registered selection retains truthful metadata.
|
||||
cp = final_cp(identity, hybrid=hybrid)
|
||||
cp.transmissionType = structs.CarParams.TransmissionType.manual
|
||||
assert not qualified(cp)
|
||||
@@ -196,9 +201,9 @@ def test_active_engagement_arms_without_raw_cruise_resumes_lateral_and_preserves
|
||||
snapshot = SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long)
|
||||
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
|
||||
lateralAllowed=True, longitudinalAllowed=False)
|
||||
kwargs = dict(standard_lateral=False, standard_longitudinal=False, intent=snapshot,
|
||||
car_state=cs.as_reader(), initialized=True, model_ready=True,
|
||||
no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=5.)
|
||||
kwargs = {"standard_lateral": False, "standard_longitudinal": False, "intent": snapshot,
|
||||
"car_state": cs.as_reader(), "initialized": True, "model_ready": True,
|
||||
"no_entry": False, "immediate_disable": False, "dm_lockout": False, "pause_brake_mps": 5.}
|
||||
decision = decide_axes(native=native, **kwargs)
|
||||
assert decision.lateral_active == (not temporary)
|
||||
assert not decision.longitudinal_active
|
||||
@@ -472,7 +477,7 @@ def test_actual_card_ack_loss_requires_current_requested_same_identity(changed):
|
||||
host = Car.__new__(Car)
|
||||
host.CP, host.sm, host.slc_producer_session = cp, sm, "producer"
|
||||
host.aol_transport_feedback = TransportPauseFeedback(cp)
|
||||
kwargs = dict(latched=True, panda_ready=True, restricted=False, now_ns=now, pending_since_ns=0)
|
||||
kwargs = {"latched": True, "panda_ready": True, "restricted": False, "now_ns": now, "pending_since_ns": 0}
|
||||
assert host.aol_honda_native_observation(native, **kwargs) == (0, False, False)
|
||||
if changed == "cold":
|
||||
host._honda_aol_acknowledgment = None
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.stock_aol import qualified
|
||||
from opendbc.car.honda.values import CAR, HondaFlags, DBC
|
||||
|
||||
|
||||
@@ -65,10 +66,11 @@ def exercise_startup(identity, alpha, release, aol, base_word, monkeypatch, firm
|
||||
initial.carFw = list(firmware)
|
||||
assert initial.safetyConfigs[0].safetyParam == base_word
|
||||
expected = initial.to_dict()
|
||||
explicit_owner = not (classic and initial.openpilotLongitudinalControl)
|
||||
expected["alternativeExperience"] = 32 if aol and explicit_owner else 0
|
||||
if aol and not explicit_owner:
|
||||
expected["safetyConfigs"][0]["safetyParam"] = base_word | 32
|
||||
classic_alpha = classic and initial.openpilotLongitudinalControl
|
||||
expected["alternativeExperience"] = 32 if aol else 0
|
||||
if aol and classic_alpha:
|
||||
# Classic alpha now uses the explicit owner: CLASSIC_LONG_AOL | AOL_BOSCH_LONG.
|
||||
expected["safetyConfigs"][0]["safetyParam"] = base_word | 2080
|
||||
if aol and identity == CAR.HONDA_ODYSSEY_TWN:
|
||||
expected["safetyConfigs"][0]["safetyParam"] = 260
|
||||
subscriber = messaging.sub_sock("carParams", timeout=100, conflate=True)
|
||||
@@ -87,7 +89,11 @@ def exercise_startup(identity, alpha, release, aol, base_word, monkeypatch, firm
|
||||
assert ci.CP is card.CP and ci.CC is not None
|
||||
assert card.aol_qualified is aol
|
||||
if aol:
|
||||
assert card.aol_card_intent.explicit_latch is explicit_owner
|
||||
assert card.aol_card_intent.explicit_latch is True
|
||||
if classic_alpha:
|
||||
legacy = card.CP.as_reader().as_builder()
|
||||
legacy.safetyConfigs[0].safetyParam = base_word | 32
|
||||
assert not qualified(legacy, marked_only=True)
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
with structs.CarParams.from_bytes(saved.get("CarParams")) as published:
|
||||
assert published.to_dict() == expected
|
||||
@@ -108,7 +114,7 @@ def exercise_startup(identity, alpha, release, aol, base_word, monkeypatch, firm
|
||||
assert intent.producerSessionId == card.slc_producer_session
|
||||
controls = Controls()
|
||||
assert controls.CP.to_dict() == expected
|
||||
assert controls.ordinary_axis_ack_required is (aol and explicit_owner)
|
||||
assert controls.ordinary_axis_ack_required is aol
|
||||
now = 1_000_000_000
|
||||
feed(controls, now, 0, active=False, enabled=False, can_valid=False, can_timeout=True)
|
||||
if aol:
|
||||
|
||||
@@ -92,6 +92,10 @@ class HondaSafetyFlags(IntFlag):
|
||||
RADARLESS = 8
|
||||
BOSCH_CANFD = 16
|
||||
BOSCH_CANFD_MVL = 64
|
||||
CLASSIC_LONG_AOL = 2048 # Exact marked DEBUG classic Bosch alpha owner.
|
||||
ALT_RADAR_AOL = 1024 # Exact marked DEBUG alpha owner; physical EPS/gear token.
|
||||
NIDEC_PEDAL_AOL = 512 # Model1 only; distinct from model20 STOCK_TJA.
|
||||
STOCK_TJA = 512 # Marked MDX4G AOL; EPS status1 is healthy TJA lockout.
|
||||
|
||||
|
||||
class HondaFlags(IntFlag):
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Correlate recent local steering rejection echoes; never authorize output."""
|
||||
from collections import deque
|
||||
import time
|
||||
|
||||
|
||||
class AngleTxRecovery:
|
||||
def __init__(self, address, bus, clock=None):
|
||||
self.clock = time.monotonic_ns if clock is None else clock
|
||||
self.address = address
|
||||
self.bus = bus
|
||||
self.sent = deque(maxlen=4)
|
||||
self.last_packet_ns = 0
|
||||
self.pending_frame = -1
|
||||
self.pending_sent_ns = -1
|
||||
|
||||
def record(self, frames, frame):
|
||||
now_ns = self.clock()
|
||||
for address, data, bus in frames:
|
||||
if address == self.address and bus == self.bus and len(data) == 32 and (data[9] >> 4 & 3) == 2:
|
||||
self.sent.append((frame, now_ns, bytes(data)))
|
||||
|
||||
def observe(self, packets, frame):
|
||||
now_ns = self.clock()
|
||||
for stamp, frames in packets:
|
||||
if stamp <= self.last_packet_ns:
|
||||
continue
|
||||
self.last_packet_ns = stamp
|
||||
for address, data, bus in frames:
|
||||
if address != self.address or bus != self.bus + 0xC0 or len(data) != 32:
|
||||
continue
|
||||
# Consume an exact recent command once. The original local send clock
|
||||
# also bounds delayed echoes when controller updates have been paused.
|
||||
match = next(((sent_frame, sent_ns, sent_data) for sent_frame, sent_ns, sent_data in self.sent
|
||||
if 0 < frame - sent_frame <= 3 and 0 <= now_ns - sent_ns <= 30_000_000 and sent_data == bytes(data)), None)
|
||||
if match is not None:
|
||||
self.sent.remove(match)
|
||||
self.pending_frame = frame
|
||||
self.pending_sent_ns = match[1]
|
||||
|
||||
def take_rejection(self, frame):
|
||||
pending = self.pending_frame == frame and 0 <= self.clock() - self.pending_sent_ns <= 30_000_000
|
||||
self.pending_frame = -1
|
||||
return pending
|
||||
@@ -112,6 +112,14 @@ def stock_word(cp):
|
||||
|
||||
|
||||
def qualified(cp, *, marked_only=False):
|
||||
# The paired EV9 LONG owner uses its own RX/TX and native policy; share only
|
||||
# the established host permission/temporary-restriction handling.
|
||||
if cp.carFingerprint == CAR.HYUNDAI_IONIQ_5_PE and cp.openpilotLongitudinalControl:
|
||||
from opendbc.car.hyundai.pe_long_aol import qualified as pe_qualified
|
||||
return pe_qualified(cp, marked_only=True)
|
||||
if cp.carFingerprint == CAR.KIA_EV9 and cp.openpilotLongitudinalControl:
|
||||
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_qualified
|
||||
return ev9_qualified(cp, marked_only=True)
|
||||
identity = str(cp.carFingerprint)
|
||||
if identity not in MODELS or cp.brand != 'hyundai':
|
||||
return False
|
||||
@@ -183,6 +191,12 @@ def command_allowed(cp, state):
|
||||
|
||||
|
||||
def temporary_restriction(cp, state):
|
||||
from opendbc.car.hyundai.pe_long_aol import qualified as pe_qualified, temporary_restriction as pe_restricted
|
||||
if pe_qualified(cp, marked_only=True):
|
||||
return pe_restricted(cp, state)
|
||||
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_qualified, temporary_restriction as ev9_restricted
|
||||
if ev9_qualified(cp, marked_only=True):
|
||||
return ev9_restricted(cp, state)
|
||||
gears = structs.CarState.GearShifter
|
||||
return bool(
|
||||
state is not None
|
||||
|
||||
@@ -29,7 +29,7 @@ def config_index(cp):
|
||||
if cp.openpilotLongitudinalControl and not cp.pcmCruise:
|
||||
if raw == 0x15 and cp.alternativeExperience == 0:
|
||||
return 1
|
||||
if cp.carFingerprint == CAR.KIA_EV6 and raw == 0x815 and cp.alternativeExperience == 32:
|
||||
if cp.carFingerprint in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and raw == 0x815 and cp.alternativeExperience == 32:
|
||||
return 1
|
||||
elif not cp.openpilotLongitudinalControl and cp.pcmCruise and cp.alternativeExperience == 0 and raw in (0x11, 0x811):
|
||||
return 1
|
||||
|
||||
@@ -97,18 +97,23 @@ def qualified(cp, *, marked_only=False):
|
||||
|
||||
LONG_AOL_WORDS = frozenset((0x0815, 0x0895))
|
||||
LONG_EV_CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN))
|
||||
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN}
|
||||
|
||||
|
||||
def qualified_long(cp, *, marked_only=False):
|
||||
if cp.carFingerprint == CAR.KIA_EV6:
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified
|
||||
return ev6_qualified(cp, marked_only=marked_only)
|
||||
if cp.carFingerprint not in LONG_EV_CARS or not cp.openpilotLongitudinalControl or cp.pcmCruise:
|
||||
if cp.carFingerprint not in INDEPENDENT_LONG_BUTTON_CARS or not cp.openpilotLongitudinalControl or cp.pcmCruise:
|
||||
return False
|
||||
if cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
||||
from opendbc.car.hyundai.gv70_startup import eligible
|
||||
if not eligible(cp):
|
||||
return False
|
||||
word = _base_word(cp, experiences=(0, 32))
|
||||
if word not in (0x11, 0x91):
|
||||
return False
|
||||
raw = int(cp.safetyConfigs[0].safetyParam)
|
||||
raw = int(cp.safetyConfigs[config_index(cp)].safetyParam)
|
||||
return (raw == (word | 0x804) and cp.alternativeExperience == 32) or (
|
||||
not marked_only and raw == (word | 4) and cp.alternativeExperience == 0
|
||||
)
|
||||
|
||||
@@ -3,9 +3,10 @@ import time
|
||||
|
||||
from opendbc.car import Bus
|
||||
from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome
|
||||
from opendbc.car.hyundai.ecu_startup import Outcome
|
||||
from opendbc.car.hyundai.ecu_continuity import ContinuousECUStartup
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.values import HyundaiFlags
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
from opendbc.car.hyundai.canfd_owner import topology_index
|
||||
|
||||
|
||||
@@ -24,7 +25,7 @@ def first_generation(cp):
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar)
|
||||
|
||||
|
||||
class CapturedADRVStartup(HyundaiECUStartup):
|
||||
class CapturedADRVStartup(ContinuousECUStartup):
|
||||
def __init__(self, cp, callbacks, *, label, template_type):
|
||||
buses = CanBus(cp)
|
||||
super().__init__(cp, callbacks, address=0x730, bus=buses.ECAN, label=label)
|
||||
@@ -33,6 +34,10 @@ class CapturedADRVStartup(HyundaiECUStartup):
|
||||
self.template_type = template_type
|
||||
self.template = None
|
||||
|
||||
def continuity_required(self):
|
||||
return (self.cp.carFingerprint in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and
|
||||
self.cp.openpilotLongitudinalControl and first_generation(self.cp))
|
||||
|
||||
def _capture(self):
|
||||
floor = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
deadline = time.monotonic() + 1.
|
||||
@@ -91,6 +96,9 @@ class CapturedADRVStartup(HyundaiECUStartup):
|
||||
return False
|
||||
return True
|
||||
|
||||
def sources_current(self, ci, now_ns):
|
||||
return self.outcome is not Outcome.SENT_UNCONFIRMED or self._sources(ci, now_ns, floor=self.floor_ns)
|
||||
|
||||
def _warm_stock(self, ci):
|
||||
deadline = time.monotonic() + 3.
|
||||
while time.monotonic() < deadline:
|
||||
|
||||
@@ -11,6 +11,8 @@ from opendbc.car.hyundai.ev9_longitudinal import (
|
||||
BlindspotWarningOutput,
|
||||
)
|
||||
from opendbc.car.hyundai import ev9_long_sender
|
||||
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_long_aol_qualified
|
||||
from opendbc.car.hyundai.pe_long_aol import qualified as pe_long_aol_qualified
|
||||
from opendbc.car.common.filter_simple import FirstOrderFilter
|
||||
from opendbc.car.lateral import get_max_angle_vm, get_max_angle_delta_vm
|
||||
import numpy as np
|
||||
@@ -23,6 +25,7 @@ from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_gas, ray_pedal_enabled
|
||||
from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy
|
||||
from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons
|
||||
from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic_stock_aol
|
||||
from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock_aol
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as qualified_ev6_aol
|
||||
from opendbc.car.hyundai.g90_longitudinal import G90LongitudinalPolicy
|
||||
@@ -31,7 +34,8 @@ from opendbc.car.hyundai.g90_lead import G90LeadState, eligible as g90_lead_elig
|
||||
from opendbc.car.hyundai.gv70_camera_lead import eligible as gv70_lead_eligible
|
||||
from opendbc.car.hyundai.canfd_lead import select_lead, ABSENT
|
||||
from opendbc.car.hyundai.torque_ev_scc import eligible as torque_ev_scc_eligible, options as torque_ev_scc_options, clock_boot_ns, fallback_lead
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import replacement_requested
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import replacement_requested, qualified as qualified_ccnc_ev_stock
|
||||
from opendbc.car.hyundai.angle_tx_recovery import AngleTxRecovery
|
||||
from opendbc.car.hyundai.values import is_blended, HyundaiFlags, HyundaiSafetyFlags, Buttons, CarControllerParams, CAR
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
@@ -98,6 +102,8 @@ def process_hud_alert(enabled, fingerprint, hud_control):
|
||||
class CarController(CarControllerBase):
|
||||
def __init__(self, dbc_names, CP):
|
||||
super().__init__(dbc_names, CP)
|
||||
from opendbc.car.hyundai.gv60_startup import qualified as gv60_long_qualified
|
||||
self.gv60_longitudinal = gv60_long_qualified(CP)
|
||||
self.CAN = CanBus(CP)
|
||||
self.params = CarControllerParams(CP)
|
||||
self.packer = CANPacker(dbc_names[Bus.pt])
|
||||
@@ -105,6 +111,9 @@ class CarController(CarControllerBase):
|
||||
self.angle_vm = VehicleModel(CP) if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else None
|
||||
self.apply_angle_last = 0.0
|
||||
self.angle_gain_last = 0.0
|
||||
self.angle_tx_recovery = (AngleTxRecovery(0x110, self.CAN.ACAN)
|
||||
if CP.carFingerprint in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9) and
|
||||
(qualified_angle_aol(CP, marked_only=True) or qualified_ccnc_ev_stock(CP)) else None)
|
||||
|
||||
self.accel_last = 0
|
||||
self.apply_torque_last = 0
|
||||
@@ -144,6 +153,8 @@ class CarController(CarControllerBase):
|
||||
self.ioniq6_bsm_enabled = ioniq6_status
|
||||
self.ev6_aol_status = CP.carFingerprint == CAR.KIA_EV6 and (qualified_canfd_stock_aol(CP) or qualified_ev6_aol(CP))
|
||||
self.canfd_dash_icons = CanFDDashIcons() if ioniq6_status or self.torque_ev_scc_enabled or self.ev6_aol_status else None
|
||||
self.classic_dash_icons = CanFDDashIcons() if qualified_classic_stock_aol(CP, marked_only=True) and CP.carFingerprint != CAR.KIA_RAY_EV else None
|
||||
self.classic_lfa_icon = None
|
||||
self.ioniq6_bsm_counter = 0
|
||||
self.ioniq6_bsm_last_can_ns = 0
|
||||
self._ray_pedal = ray_pedal_enabled(CP)
|
||||
@@ -157,9 +168,10 @@ class CarController(CarControllerBase):
|
||||
self._ray_disengage_frame = None
|
||||
|
||||
def update(self, CC, CS, now_nanos):
|
||||
if self.pe_longitudinal is not None and (self.pe_startup is None or not self.pe_startup.can_emit(self)):
|
||||
pending_startup_required = self.pe_longitudinal is not None or self.ev9_longitudinal is not None
|
||||
if pending_startup_required and (self.pe_startup is None or not self.pe_startup.can_emit(self)):
|
||||
return structs.CarControl.Actuators.new_message(), []
|
||||
if self.pe_longitudinal is not None and not self.pe_startup.handed_off:
|
||||
if pending_startup_required and not self.pe_startup.handed_off:
|
||||
CC = structs.CarControl.new_message().as_reader()
|
||||
_projection = self.learning_projection_observer
|
||||
if _projection is not None:
|
||||
@@ -168,6 +180,11 @@ class CarController(CarControllerBase):
|
||||
if self.CP.carFingerprint == CAR.KIA_EV6_2025 and self.CP.dashcamOnly:
|
||||
return actuators.as_builder(), []
|
||||
hud_control = CC.hudControl
|
||||
# Card finalizes the AOL safety marker after CarInterface constructs us.
|
||||
if self.classic_dash_icons is None and self.CP.carFingerprint != CAR.KIA_RAY_EV and qualified_classic_stock_aol(self.CP, marked_only=True):
|
||||
self.classic_dash_icons = CanFDDashIcons()
|
||||
if self.classic_dash_icons is not None:
|
||||
self.classic_lfa_icon = self.classic_dash_icons.update(self.frame, CC.enabled, CC.latActive)
|
||||
if self.CP.carFingerprint == CAR.KIA_RAY_EV:
|
||||
if CC.latActive:
|
||||
self._ray_disengage_frame = None
|
||||
@@ -266,6 +283,8 @@ class CarController(CarControllerBase):
|
||||
new_actuators.steeringAngleDeg = self.apply_angle_last
|
||||
new_actuators.accel = accel
|
||||
|
||||
if self.angle_tx_recovery is not None:
|
||||
self.angle_tx_recovery.record(can_sends, self.frame)
|
||||
self.frame += 1
|
||||
if _projection is not None:
|
||||
_projection.finish(new_actuators.torque)
|
||||
@@ -316,7 +335,8 @@ class CarController(CarControllerBase):
|
||||
torque_fault, CS.lkas11, sys_warning, sys_state, hud_enabled,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
left_lane_warning, right_lane_warning,
|
||||
ray_lka_icon=(self._ray_lfa_icon or 1) if self.CP.carFingerprint == CAR.KIA_RAY_EV else None))
|
||||
ray_lka_icon=(self._ray_lfa_icon or 1) if self.CP.carFingerprint == CAR.KIA_RAY_EV else None,
|
||||
aol_lka_icon=(self.classic_lfa_icon or 1) if self.classic_lfa_icon is not None else None))
|
||||
if _projection is not None and can_sends:
|
||||
if not is_blended(self.CP) and (self.CP.carFingerprint != CAR.KIA_RAY_EV or self._ray_lkas11_active):
|
||||
_projection.emit('hyundai.LKAS11', apply_torque, apply_steer_req, can_sends[-1])
|
||||
@@ -365,7 +385,7 @@ class CarController(CarControllerBase):
|
||||
lfa_icon = 2 if CC.enabled or CC.latActive else 3 if disengaging else 0
|
||||
can_sends.append(hyundaican.create_blended_lfahda(self.packer, self.CAN, self.frame, lfa_icon))
|
||||
else:
|
||||
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled))
|
||||
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled, lfa_icon=self.classic_lfa_icon))
|
||||
|
||||
# 5 Hz ACC options
|
||||
if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl and not self._ray_pedal and self.blended_longitudinal is None:
|
||||
@@ -410,7 +430,9 @@ class CarController(CarControllerBase):
|
||||
safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0)
|
||||
bound = min(get_max_angle_vm(safety_speed, self.ev9_legacy_envelope, self.params),
|
||||
get_max_angle_vm(safety_speed, self.angle_vm, self.params))
|
||||
ev9_lateral_allowed = lateral_request_allowed(CC, CS.out, CS.angle_steering_fault)
|
||||
ev9_lateral_allowed = lateral_request_allowed(CC, CS.out, CS.angle_steering_fault,
|
||||
independent=(ev9_long_aol_qualified(self.CP, marked_only=True) or
|
||||
pe_long_aol_qualified(self.CP, marked_only=True)))
|
||||
active = bool(ev9_lateral_allowed and CC.latActive and abs(measured) <= bound and abs(self.apply_angle_last) <= bound)
|
||||
self.ev9_angle_filter.update_alpha(float(np.interp(CS.out.vEgo, [5.0, 10.0, 20.0], [0.2, 0.1, 0.0])))
|
||||
desired = self.ev9_angle_filter.update(float(np.clip(CC.actuators.steeringAngleDeg,
|
||||
@@ -453,6 +475,9 @@ class CarController(CarControllerBase):
|
||||
angle_active = angle_aol_active
|
||||
if ccnc_ev_stock:
|
||||
angle_active = ccnc_ev_replacement
|
||||
if self.angle_tx_recovery is not None and self.angle_tx_recovery.take_rejection(self.frame):
|
||||
self.apply_angle_last = CS.out.steeringAngleDeg
|
||||
self.angle_gain_last = 0.0
|
||||
desired_angle = float(np.clip(CC.actuators.steeringAngleDeg, -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX,
|
||||
self.params.ANGLE_LIMITS.STEER_ANGLE_MAX))
|
||||
safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0)
|
||||
@@ -545,6 +570,8 @@ class CarController(CarControllerBase):
|
||||
self.packer, self.CAN, self.frame, template=self.adrv_template,
|
||||
drive_gear=CS.out.gearShifter == structs.CarState.GearShifter.drive,
|
||||
speed=CS.out.vEgoRaw if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else None))
|
||||
if self.gv60_longitudinal and self.frame % 4 == 0:
|
||||
can_sends.append(hyundaicanfd.create_gv60_radar_heartbeat(self.frame // 4, CS.out.brakePressed, CS.out.gasPressed))
|
||||
if self.ioniq6_longitudinal is not None and self.frame % 4 == 0:
|
||||
can_sends.append(hyundaicanfd.create_ioniq6_radar_heartbeat(self.frame // 4, CS.out.brakePressed, CS.out.gasPressed))
|
||||
if self.ioniq6_bsm_enabled and self.frame % 5 == 0:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from opendbc.car.hyundai.left_paddle import has_left_paddle
|
||||
from opendbc.car.hyundai.ioniq5pe_long_profile import qualified as pe_long_qualified
|
||||
from opendbc.car.hyundai.ioniq5pe_ready import PEReadySource
|
||||
from opendbc.car.hyundai.blended_stock_aol import qualified as qualified_blended_stock, qualified_alpha as qualified_blended_alpha, MixedStockLkasSources
|
||||
@@ -7,6 +8,8 @@ from opendbc.car.hyundai.ev9_camera_lead import EV9CameraLead
|
||||
from opendbc.car.hyundai.canfd_camera_lead import CANFDCameraLead
|
||||
from opendbc.car.hyundai.torque_ev_scc import eligible as torque_ev_scc_eligible
|
||||
from opendbc.car.hyundai.ev9_longitudinal import qualified as ev9_long_qualified
|
||||
from opendbc.car.hyundai.canfd_angle_aol import qualified as angle_aol_qualified
|
||||
from opendbc.car.hyundai.long_buttons import LongCruiseButtons, qualified as long_buttons_qualified
|
||||
from collections import deque
|
||||
import copy
|
||||
import math
|
||||
@@ -64,8 +67,10 @@ class CarState(CarStateBase):
|
||||
self.forte_lkas_sources = ClassicSccLkasSources(CP.carFingerprint)
|
||||
if qualified_blended_stock(CP) or qualified_blended_alpha(CP):
|
||||
self.forte_lkas_sources = MixedStockLkasSources(CP)
|
||||
self.long_buttons = (LongCruiseButtons(track_main=CP.carFingerprint != CAR.HYUNDAI_IONIQ_5_PE)
|
||||
if long_buttons_qualified(CP) else None)
|
||||
self.pe_long = pe_long_qualified(CP)
|
||||
self.pe_ready = PEReadySource() if self.pe_long else None
|
||||
self.pe_ready = PEReadySource() if self.pe_long or ev9_long_qualified(CP) else None
|
||||
self.ev9_long = ev9_long_qualified(CP) or self.pe_long
|
||||
self.ev9_camera_lead = (CANFDCameraLead(CP, CanBus(CP).ECAN) if self.pe_long else
|
||||
EV9CameraLead(CP) if self.ev9_long else None)
|
||||
@@ -161,10 +166,23 @@ class CarState(CarStateBase):
|
||||
return not any(cp.vl[name][signal] != 0 or any(value != 0 for value in cp.vl_all[name][signal])
|
||||
for name, signal in signals)
|
||||
|
||||
def update_button_enable(self, buttonEvents):
|
||||
if self.long_buttons is not None:
|
||||
return self.long_buttons.button_enable
|
||||
return super().update_button_enable(buttonEvents)
|
||||
|
||||
def create_main_button_events(self, cur_button, prev_button):
|
||||
if self.long_buttons is not None:
|
||||
return [event for current, previous in self.long_buttons.main_transitions
|
||||
for event in create_button_events(current, previous, {1: ButtonType.mainCruise})]
|
||||
return create_button_events(cur_button, prev_button, {1: ButtonType.mainCruise})
|
||||
|
||||
def create_cruise_button_events(self, cur_button, prev_button, samples=()):
|
||||
# Preserve every real edge in an alpha parser batch, including a complete
|
||||
# Cancel press/release while the host is already enabled.
|
||||
if is_blended(self.CP) and self.CP.openpilotLongitudinalControl:
|
||||
if self.long_buttons is not None:
|
||||
return [event for current, previous in self.long_buttons.cruise_transitions
|
||||
for event in create_button_events(current, previous, BUTTONS_DICT)]
|
||||
if (is_blended(self.CP) and self.CP.openpilotLongitudinalControl or
|
||||
samples and self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and angle_aol_qualified(self.CP, marked_only=True)):
|
||||
events = []
|
||||
for button in samples:
|
||||
events.extend(create_button_events(int(button), prev_button, BUTTONS_DICT))
|
||||
@@ -326,6 +344,31 @@ class CarState(CarStateBase):
|
||||
else:
|
||||
self.lkas11 = copy.copy(cp_cam.vl["LKAS11"])
|
||||
self.clu11 = copy.copy(cp.vl["CLU11"])
|
||||
from opendbc.car.hyundai.classic_long_aol import independent_buttons_qualified
|
||||
self.ev6_aol_samples = ()
|
||||
self.ev6_aol_source_healthy = False
|
||||
if independent_buttons_qualified(self.CP):
|
||||
from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
source = cp.message_states.get(cp.dbc.name_to_msg['CLU11'].address)
|
||||
self.ev6_aol_sample_stamp_ns = int(source.timestamps[-1]) if source is not None and source.timestamps else 0
|
||||
self.ev6_aol_timeout_ns = int(source.timeout_threshold) if source is not None else 0
|
||||
now = cp._last_update_nanos
|
||||
bus_timeout = cp.bus_timeout
|
||||
self.ev6_aol_source_healthy = bool(
|
||||
not bus_timeout and source is not None and source.timestamps
|
||||
and source.valid(now, bus_timeout) and source.counter_fail < MAX_BAD_COUNTER
|
||||
and 0 < self.ev6_aol_sample_stamp_ns <= now
|
||||
and now - self.ev6_aol_sample_stamp_ns <= source.timeout_threshold)
|
||||
values = cp.vl_all['CLU11']
|
||||
self.ev6_aol_samples = tuple((main, 0, cruise) for main, cruise in zip(
|
||||
values['CF_Clu_CruiseSwMain'], values['CF_Clu_CruiseSwState'], strict=True))
|
||||
|
||||
if self.long_buttons is not None:
|
||||
values = cp.vl_all['CLU11']
|
||||
self.long_buttons.observe((main, False, cruise) for main, cruise in zip(
|
||||
values['CF_Clu_CruiseSwMain'], values['CF_Clu_CruiseSwState'], strict=True))
|
||||
ret.cruiseState.available = self.long_buttons.available(ret.cruiseState.available)
|
||||
|
||||
self.steer_state = cp.vl["MDPS12"]["CF_Mdps_ToiActive"] # 0 NOT ACTIVE, 1 ACTIVE
|
||||
prev_cruise_buttons = self.cruise_buttons[-1]
|
||||
prev_main_buttons = self.main_buttons[-1]
|
||||
@@ -348,7 +391,7 @@ class CarState(CarStateBase):
|
||||
|
||||
button_events = [*self.create_cruise_button_events(self.cruise_buttons[-1], prev_cruise_buttons,
|
||||
cp.vl_all["CLU11"]["CF_Clu_CruiseSwState"]),
|
||||
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
|
||||
*self.create_main_button_events(self.main_buttons[-1], prev_main_buttons),
|
||||
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})]
|
||||
|
||||
if self.forte_lkas_sources is not None and getattr(self.forte_lkas_sources, 'active', True):
|
||||
@@ -498,9 +541,11 @@ class CarState(CarStateBase):
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as ev6_aol_qualified
|
||||
self.ev6_aol_samples = ()
|
||||
self.ev6_aol_source_healthy = False
|
||||
from opendbc.car.hyundai.canfd_stock_aol import LONG_EV_CARS, qualified_long
|
||||
if ev6_aol_qualified(self.CP) or (
|
||||
self.CP.carFingerprint in LONG_EV_CARS and qualified_long(self.CP, marked_only=True)):
|
||||
from opendbc.car.hyundai.canfd_stock_aol import INDEPENDENT_LONG_BUTTON_CARS, qualified_long
|
||||
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_long_aol_qualified
|
||||
from opendbc.car.hyundai.pe_long_aol import qualified as pe_long_aol_qualified
|
||||
if ev6_aol_qualified(self.CP) or ev9_long_aol_qualified(self.CP, marked_only=True) or pe_long_aol_qualified(self.CP, marked_only=True) or (
|
||||
self.CP.carFingerprint in INDEPENDENT_LONG_BUTTON_CARS and qualified_long(self.CP, marked_only=True)):
|
||||
from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
source = cp.message_states.get(cp.dbc.name_to_msg[self.cruise_btns_msg_canfd].address)
|
||||
self.ev6_aol_sample_stamp_ns = int(source.timestamps[-1]) if source is not None and source.timestamps else 0
|
||||
@@ -514,6 +559,11 @@ class CarState(CarStateBase):
|
||||
and now - self.ev6_aol_sample_stamp_ns <= source.timeout_threshold)
|
||||
values = cp.vl_all[self.cruise_btns_msg_canfd]
|
||||
self.ev6_aol_samples = tuple(zip(values['ADAPTIVE_CRUISE_MAIN_BTN'], values['LDA_BTN'], values['CRUISE_BUTTONS'], strict=True))
|
||||
if self.long_buttons is not None:
|
||||
values = cp.vl_all[self.cruise_btns_msg_canfd]
|
||||
self.long_buttons.observe(zip(values['ADAPTIVE_CRUISE_MAIN_BTN'], values['NORMAL_CRUISE_MAIN_BTN'],
|
||||
values['CRUISE_BUTTONS'], strict=True))
|
||||
ret.cruiseState.available = self.long_buttons.available(ret.cruiseState.available)
|
||||
prev_cruise_buttons = self.cruise_buttons[-1]
|
||||
prev_main_buttons = self.main_buttons[-1]
|
||||
prev_lda_button = self.lda_button
|
||||
@@ -521,7 +571,7 @@ class CarState(CarStateBase):
|
||||
self.main_buttons.extend(cp.vl_all[self.cruise_btns_msg_canfd]["ADAPTIVE_CRUISE_MAIN_BTN"])
|
||||
self.lda_button = cp.vl[self.cruise_btns_msg_canfd]["LDA_BTN"]
|
||||
prev_left_paddle = self.left_paddle
|
||||
self.left_paddle = cp.vl["CRUISE_BUTTONS"]["LEFT_PADDLE"] if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 else 0
|
||||
self.left_paddle = cp.vl["CRUISE_BUTTONS"]["LEFT_PADDLE"] if has_left_paddle(self.CP) else 0
|
||||
self.buttons_counter = cp.vl[self.cruise_btns_msg_canfd]["COUNTER"]
|
||||
if not self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and self.CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME:
|
||||
self.cruise_buttons_alt_ts_ns = cp.ts_nanos["CRUISE_BUTTONS_ALT"]["COUNTER"]
|
||||
@@ -535,8 +585,9 @@ class CarState(CarStateBase):
|
||||
if self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING:
|
||||
self.angle_lkas_status = copy.copy(cp_cam.vl["LKAS_ALT" if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS"])
|
||||
|
||||
ret.buttonEvents = [*create_button_events(self.cruise_buttons[-1], prev_cruise_buttons, BUTTONS_DICT),
|
||||
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
|
||||
ret.buttonEvents = [*self.create_cruise_button_events(self.cruise_buttons[-1], prev_cruise_buttons,
|
||||
cp.vl_all[self.cruise_btns_msg_canfd]["CRUISE_BUTTONS"]),
|
||||
*self.create_main_button_events(self.main_buttons[-1], prev_main_buttons),
|
||||
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}),
|
||||
*create_button_events(self.left_paddle, prev_left_paddle, {1: ButtonType.altButton2})]
|
||||
|
||||
|
||||
@@ -69,3 +69,9 @@ def native_accepts(cp, model, param):
|
||||
and model == int(cp.safetyConfigs[0].safetyModel.raw)
|
||||
and param == aol_word(cp)
|
||||
)
|
||||
|
||||
|
||||
def independent_buttons_qualified(cp, *, marked_only=True):
|
||||
return bool(cp.carFingerprint in CLASSIC_LONG_IDS and qualified(cp, marked_only=marked_only) and
|
||||
cp.alternativeExperience in ((AOL_EXPERIENCE,) if marked_only else (0, AOL_EXPERIENCE)) and
|
||||
aol_word(cp) in (0xC04, 0xC05, 0xC06, 0xC44, 0xC45, 0xC46))
|
||||
|
||||
@@ -6,20 +6,29 @@ NORMAL_BUS_LAYOUT = {(2, 0x110): 32, (2, 0x362): 32, (1, 0x35): 32,
|
||||
(1, 0x1CF): 8, (0, 0x3A5): 24}
|
||||
|
||||
|
||||
# Classic HKG steering, chassis and stock SCC are already visible on normal
|
||||
# harness wiring. This selects a diagnostic query family, never a vehicle or
|
||||
# control profile. Fresh firmware and CAN fingerprinting still own identity.
|
||||
CLASSIC_NORMAL_BUS_LAYOUT = {(0, 0x251): 8, (0, 0x386): 8, (0, 0x394): 8,
|
||||
(0, 0x4F1): 4, (2, 0x340): 8, (0, 0x420): 8, (0, 0x421): 8}
|
||||
|
||||
|
||||
def normal_bus_topology(can_recv, *, clock=time.monotonic):
|
||||
deadline = clock() + .12
|
||||
observed = {}
|
||||
layouts = (NORMAL_BUS_LAYOUT, CLASSIC_NORMAL_BUS_LAYOUT)
|
||||
observed = [{} for _ in layouts]
|
||||
for _ in range(24):
|
||||
if clock() >= deadline:
|
||||
break
|
||||
for packet in can_recv(wait_for_one=True):
|
||||
for frame in packet:
|
||||
key = (frame.src, frame.address)
|
||||
if key in NORMAL_BUS_LAYOUT:
|
||||
length = len(frame.dat)
|
||||
if length != NORMAL_BUS_LAYOUT[key]:
|
||||
return False
|
||||
observed[key] = length
|
||||
if observed == NORMAL_BUS_LAYOUT:
|
||||
for layout, received in zip(layouts, observed, strict=True):
|
||||
if key in layout:
|
||||
length = len(frame.dat)
|
||||
if length != layout[key]:
|
||||
return False
|
||||
received[key] = length
|
||||
if any(received == layout for layout, received in zip(layouts, observed, strict=True)):
|
||||
return True
|
||||
return False
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user