mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-05 20:13:47 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a3b5e2a5 | |||
| 8ac6b224d7 |
@@ -49,11 +49,3 @@ openpilot/system/loggerd/loggerd -text
|
||||
openpilot/starpilot/galaxy/bin/frpc_linux_arm64 -text
|
||||
openpilot/starpilot/maps/provider/mapd -text
|
||||
openpilot/selfdrive/test/process_replay/refs/starpilot/*.zst -text
|
||||
|
||||
# Published Panda object files are AArch64 ELF, not text.
|
||||
openpilot/selfdrive/pandad/main.o -text
|
||||
openpilot/selfdrive/pandad/panda.o -text
|
||||
openpilot/selfdrive/pandad/panda_safety.o -text
|
||||
openpilot/selfdrive/pandad/panda_usb.o -text
|
||||
openpilot/selfdrive/pandad/pandad.o -text
|
||||
openpilot/selfdrive/pandad/spi.o -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
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- Domathon
|
||||
- Dom
|
||||
- SecretGoodStarPilot
|
||||
pull_request:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -29,11 +29,6 @@ jobs:
|
||||
steps:
|
||||
- uses: commaai/timeout@v1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
/docs/
|
||||
/openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
# Build
|
||||
- name: Build docs
|
||||
|
||||
@@ -2,7 +2,7 @@ name: iOS companion
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [Dom, secretgood-ios-integration]
|
||||
branches: [SecretGoodStarPilot, secretgood-ios-integration]
|
||||
paths:
|
||||
- 'openpilot/starpilot/companion/ios/**'
|
||||
- 'openpilot/starpilot/galaxy_bluetooth/**'
|
||||
|
||||
@@ -2,7 +2,7 @@ name: safety qualification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, Domathon, Dom]
|
||||
branches: [master, Domathon, SecretGoodStarPilot]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- Domathon
|
||||
- Dom
|
||||
- SecretGoodStarPilot
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Validate published device build receipt
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/Dom'
|
||||
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
|
||||
@@ -207,15 +207,6 @@ jobs:
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 5
|
||||
run: tools/op.sh test opendbc_repo/opendbc/car/gm/tests --json-output gm-test-results.json
|
||||
- name: Test shared GM discovery, routing, and manual overrides
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 2
|
||||
run: >-
|
||||
.venv/bin/python -m pytest -q --import-mode=importlib
|
||||
opendbc_repo/opendbc/car/gm/tests/test_shared_discovery.py
|
||||
opendbc_repo/opendbc/car/gm/tests/test_automatic_discovery.py
|
||||
opendbc_repo/opendbc/car/gm/tests/test_bolt_vin_discovery.py
|
||||
--junitxml=gm-discovery-results.xml
|
||||
- name: Test native independent-axis protocol
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
run: |
|
||||
@@ -236,14 +227,13 @@ 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') != '' || hashFiles('gm-discovery-results.xml') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
|
||||
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
|
||||
gm-discovery-results.xml
|
||||
ci-results/host-test-diagnostics/
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -49,8 +49,6 @@ bin/
|
||||
!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]
|
||||
!/openpilot/starpilot/system/starport/
|
||||
!/openpilot/tools/starport/
|
||||
*.unchunked
|
||||
*.clb
|
||||
*.class
|
||||
|
||||
+1
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
## Jetlink external inference
|
||||
|
||||
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `bf4dc940e98f5d3ed9b7163adb021439010d634f` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
|
||||
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `d24328d849b0e5154b55f149dc18c5dfe8bd79cc` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
|
||||
|
||||
## MRR35 Hyundai angle and radar
|
||||
|
||||
@@ -33,7 +33,3 @@ Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selecte
|
||||
## 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.
|
||||
|
||||
## Ford optional pinion measurement
|
||||
|
||||
The default-off Use Pinion Yaw Sensor measurement option is adapted from jrneulight’s BluePilot PR #145 (https://github.com/BluePilotDev/bluepilot/pull/145), merged as `fe5b019568c8c13fb894dc6d8d779d60985051a8`. Its per-platform geometry, native pinion measurement, integrity requirements and host vehicle-model compensation are retained. StarPilot adapts the option to its existing exact safety-profile word, modern curvature checks, AOL authority and Galaxy/native settings; the upstream maintainers do not maintain this integration. Existing upstream copyright and license notices remain in THIRD_PARTY_NOTICES.md.
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,90 +1,16 @@
|
||||
# StarPilot
|
||||
|
||||
StarPilot is an independent fork of [openpilot](https://github.com/commaai/openpilot), with custom driving controls, vehicle integrations, raylib interfaces for comma 3 and comma four, and the Galaxy companion interface.
|
||||
|
||||
[](https://deepwiki.com/StarPilotHQ/StarPilot)
|
||||
[](https://firestar.link/discord)
|
||||
[](https://github.com/StarPilotHQ/StarPilot)
|
||||
[](https://wiki.firestar.link)
|
||||
[](https://apps.apple.com/ca/app/starpilot-bt/id6821039119)
|
||||
[](https://groups.google.com/g/galaxy-starpilot-android-beta)
|
||||
**Domathon is the development branch for StarPilot 7.0.** It rebuilds StarPilot on a current openpilot foundation. Feature migration and vehicle qualification are still in progress; this branch is not a completed replacement for the existing Dom release.
|
||||
|
||||
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
|
||||
an open source driver assistance system.
|
||||
## Explore the project
|
||||
|
||||
**This is the StarPilot 7.0 development branch (`SecretGoodStarPilot`). Feature migration,
|
||||
vehicle qualification, and Auto Learn validation are still in progress.**
|
||||
|
||||
Openpilot provides
|
||||
* Automated Lane Centering
|
||||
* Adaptive Cruise Control
|
||||
* Lane Change Assist
|
||||
* Driver Monitoring *without wheel nags*
|
||||
|
||||
Ford-specific lateral-control and vehicle-support work includes substantial adaptations from
|
||||
[BluePilot](https://github.com/BluePilotDev/bluepilot/tree/bp-7.0), principally developed by
|
||||
[Alan Polk](https://github.com/alan-polk) with additional BluePilot contributors. StarPilot's
|
||||
implementation has since diverged, but that does not erase its lineage. See [CREDITS.md](CREDITS.md)
|
||||
for the code-level provenance and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for applicable
|
||||
upstream notices and terms. BluePilot and its contributors do not maintain or endorse StarPilot;
|
||||
please direct support requests for this adaptation to the StarPilot project.
|
||||
|
||||
Hyundai, Kia, and Genesis angle steering and related vehicle support include substantial adaptations
|
||||
from [sunnypilot](https://github.com/sunnypilot/sunnypilot/tree/hkg-angle-steering-2025) and its
|
||||
[opendbc angle-steering branch](https://github.com/sunnypilot/opendbc/tree/hkg-angle-steering-2025).
|
||||
StarPilot's implementation has diverged significantly; the upstream contributors do not maintain
|
||||
this adaptation. Detailed code lineage is recorded in [CREDITS.md](CREDITS.md).
|
||||
|
||||
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
|
||||
Stop by to chat or ask questions!
|
||||
If your car isn't driving as well as you'd like, [open a ticket so we can help](https://firestar.link/logs)!
|
||||
|
||||
## Documentation
|
||||
|
||||
Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists,
|
||||
installation guides, and software configuration.
|
||||
|
||||
## Features
|
||||
|
||||
* **Full support for Comma C3, C3X, and C4**
|
||||
* Full support for chestnut/eGPU on C3, C3X, and C4. C3/C3X may require special gps handling.
|
||||
* Model switcher with all of comma's tinygrad driving models
|
||||
* StarPilot custom torque controller
|
||||
* StarPilot custom longitudinal planner for both radar and VoACC (visual only, radar-less) vehicles
|
||||
* **Custom tuning for an expanding list of vehicles**
|
||||
* Firestar Lateral Method -
|
||||
Auto tuning system to adapt openpilot to *your* car's unique lateral characteristics.
|
||||
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
|
||||
Download models, change settings, update software, diagnostics, manage dashcam video, and more.
|
||||
* Navigation - Display routes and give the driving model hints on taking exits and turns.
|
||||
* Always On Lateral (full time steering assist)
|
||||
* Lane Centering - Enable to center between detected lane lines,
|
||||
with graceful fallback to model E2E lateral outputs and safety breakout
|
||||
* Force Stop 2.0 - Better stops at stop signs and traffic lights.
|
||||
* Speed Limit Controller - Use dashboard, map data, and/or vision detection for speed limit signs
|
||||
* Curve Speed Controller - Learns from how you take curves
|
||||
* Conditional Experimental Mode (CEM)
|
||||
* Conditional Chill Mode (CCM)
|
||||
* Driving Profiles and Traffic Mode
|
||||
* Custom themes
|
||||
* Alert volume customization
|
||||
* Comma Pedal Interceptor support
|
||||
* Bluetooth support - Connect keyboards, controllers, steering wheel remotes, and more.
|
||||
* Sentry mode - Record when motion is detected while parked.
|
||||
* V-ASM - Detect vehicles in the adjacent/blind spots using the driver-facing camera.
|
||||
* Additional car support - Toyota SDSU, ZSS, CAN filter; pre-AP tesla; ford; Polestar 2; and more.
|
||||
* And more!
|
||||
|
||||
|
||||
## GM-only Features
|
||||
|
||||
* Increased LKAS fault resiliency
|
||||
* ASCM_INT and SASCM support
|
||||
* Custom lateral torque controller, with special tuning for Bolts
|
||||
* 50% extra torque on 2017 Chevy Bolt
|
||||
* Improved lateral and longitudinal tuning
|
||||
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
|
||||
* Extra steering wheel button functionality for vehicles with pedal interceptor
|
||||
* Optional toggle to boot comma when remote starting your vehicle
|
||||
- [Developer commands](tools/STARPILOT_DEVELOPMENT.md): host tools, replay, and device builds.
|
||||
- [Driving interfaces](openpilot/starpilot/ui/README.md) and [Galaxy](openpilot/starpilot/galaxy/README.md): native and companion controls.
|
||||
- [Driving models](openpilot/starpilot/models/README.md): model management, artifact compatibility, and Model Laboratory status.
|
||||
- [Builds and developer tools](docs/how-to/laptop-device-build.md): local development and device builds.
|
||||
- [Dependency maintenance](tools/vendor/README.md): tracked source folders and upstream sync records.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -110,10 +36,8 @@ StarPilot retains upstream vehicle and Panda safety interfaces and adds regressi
|
||||
|
||||
Support depends on the vehicle configuration and enabled feature. An existing vehicle identifier or settings page does not by itself establish driving support. Model Laboratory currently exposes management controls, but paired driving inference remains unavailable pending integration and Chestnut validation.
|
||||
|
||||
## Third-Party Notices
|
||||
## Upstream and attribution
|
||||
|
||||
* Portions of this software include modified versions of the Material Design Icons provided by Google under the Apache License 2.0. A copy of the license is included in the `LICENSE-MDI` file.
|
||||
* Ford support includes software adapted from BluePilot's `bp-7.0` branch. The source repository contains both a standard MIT notice and a separate custom SUNNYPILOT LLC notice; StarPilot preserves both in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
* Hyundai, Kia, and Genesis support includes software adapted directly from sunnypilot's HKG angle-steering branch and sunnypilot/opendbc. StarPilot preserves the applicable notices in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
The current openpilot baseline is [`78dccf0b482ea1298fd1f6fa59b0554187b02ce6`](https://github.com/commaai/openpilot/commit/78dccf0b482ea1298fd1f6fa59b0554187b02ce6). Its history remains intact, followed by the StarPilot foundation and feature commits. Upstream development continues independently; the baseline changes only after an update has been integrated and checked.
|
||||
|
||||
> This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
|
||||
See [LICENSE](LICENSE), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and [CREDITS.md](CREDITS.md) for licensing and source attribution. Component licenses remain with their source folders. StarPilot is maintained independently of comma.
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ SConscript([
|
||||
|
||||
if arch == "comma_arm64":
|
||||
SConscript(['openpilot/system/camerad/SConscript'])
|
||||
SConscript(['openpilot/starpilot/system/starport/SConscript'])
|
||||
SConscript(['openpilot/starpilot/system/android_auto/SConscript'])
|
||||
|
||||
# Build selfdrive
|
||||
SConscript([
|
||||
|
||||
@@ -129,7 +129,7 @@ UI framework is bundled.
|
||||
|
||||
## Galaxy Bluetooth companion source snapshot
|
||||
|
||||
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/StarPilotHQ/StarPilot
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_BIN="${HEATDEATH_PYTHON:-python3}"
|
||||
if [[ -z "${HEATDEATH_PYTHON:-}" && -x /usr/local/venv/bin/python3 ]]; then
|
||||
PYTHON_BIN=/usr/local/venv/bin/python3
|
||||
fi
|
||||
exec "$PYTHON_BIN" -B "$ROOT_DIR/tools/heatdeath/heatdeath.py" "$@"
|
||||
@@ -10,4 +10,4 @@ jetlink: run openpilot's large driving models on an attached Jetson.
|
||||
# so does the release check (macos/scripts/check-version.sh). The comma runs
|
||||
# jetlink from a checkout, not an install, so this is also all it has. The Swift
|
||||
# reads it from Pinned.swift: run JetlinkKit/Scripts/make_pins.py after a bump.
|
||||
__version__ = '0.8.9'
|
||||
__version__ = '0.8.7'
|
||||
|
||||
@@ -152,16 +152,6 @@ class ModelSide(WorkerSide, Protocol):
|
||||
frame thread at a hand-back, so it zeroes the history in place and
|
||||
allocates and compiles nothing; warp.reset_queues makes one."""
|
||||
|
||||
def model_warp(self, small: Any, cam_w: int, cam_h: int, model_w: int, model_h: int) -> FrameWarp | None:
|
||||
"""Optional (OPTIONAL): the warp the link sends frames through, warmed,
|
||||
or None for openpilot's driving warp pickle at warp_path (warp.Warp). For
|
||||
a fork whose runtime cannot load that pickle, typically with its small
|
||||
model's own warp. Asked once on modeld's main thread before the frame
|
||||
loop, after the small model's warmup; raising keeps the drive on the
|
||||
small model, as a pickle that does not load does. The offroad checks
|
||||
still look for the pickle (Warps.built): only modeld has a small model
|
||||
to ask, so warp_path must name a file either way."""
|
||||
|
||||
|
||||
class Openpilot(ModelSide, Protocol):
|
||||
"""The whole adapter: one object implements every side."""
|
||||
@@ -208,28 +198,13 @@ class JoinedModel(Protocol):
|
||||
"""Let go of the link at modeld's exit."""
|
||||
|
||||
|
||||
class FrameWarp(Protocol):
|
||||
"""What an adapter's model_warp returns, warp.Warp's shape, run on modeld's
|
||||
frame thread. Only one of the small and the large model runs a frame, so a
|
||||
warp that shares the small model's JIT and buffers is never in use by both.
|
||||
On a comma, a warp that reads its GPU output back moves it first with
|
||||
warp.make_coherent, before the warp's JIT first runs."""
|
||||
output: Any # the warped frame, (2, 6, model_h/2, model_w/2) uint8 as bytes: read after wait(), until the next start()
|
||||
|
||||
def start(self, frame: int, big_frame: int, tfm: Any, big_tfm: Any) -> None:
|
||||
"""Warp the camera buffers at these addresses under their 3x3 transforms."""
|
||||
|
||||
def wait(self) -> None:
|
||||
"""Return once `output` holds what start() warped."""
|
||||
|
||||
|
||||
# The members of each protocol an implementation may lack: jetlink reads them
|
||||
# with getattr and a fallback. conformance() passes an object without one and
|
||||
# holds one with it to its signature, and adding one keeps API. Kept out of
|
||||
# the protocols, where a name would be a member itself; a type checker still
|
||||
# asks for these, as typing has no optional member
|
||||
OPTIONAL: dict[type, tuple[str, ...]] = {
|
||||
ModelSide: ('small_reset', 'model_warp'),
|
||||
ModelSide: ('small_reset',),
|
||||
SmallModel: ('desire_key', 'numpy_inputs', 'lat_delay', 'PLANPLUS_CONTROL'),
|
||||
JoinedModel: ('get_action_from_model', 'constants', 'LAT_SMOOTH_SECONDS', 'LONG_SMOOTH_SECONDS', 'lat_delay'),
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ class JoiningModelState:
|
||||
self._demote(BEHIND)
|
||||
elif self._active is not self._small and self._frame_drop_ratio > DROP_LIMIT:
|
||||
# modeld is behind the large model; this frame is the small model's
|
||||
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, " +
|
||||
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, "
|
||||
"the small model drives from this one", self._frame_drop_ratio * 100)
|
||||
self._demote(BEHIND)
|
||||
self._maybe_swap()
|
||||
@@ -745,7 +745,6 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
|
||||
link = links.Link(parts.log)
|
||||
|
||||
def prepare():
|
||||
from jetlink.openpilot.interface import FrameWarp, conformance
|
||||
from jetlink.openpilot.warp import Warp, prepare_reset
|
||||
# the gadget first, so the Jetson enumerates while the warp loads. Left to
|
||||
# the join thread the bind lands ~3 s later, behind the small model's
|
||||
@@ -760,12 +759,7 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
|
||||
try:
|
||||
# the fork's own reset of its small model, else jetlink's from its shape
|
||||
ready['reset_small'] = getattr(op, 'small_reset', lambda small: None)(small) or prepare_reset(small)
|
||||
# the fork's own warp, else openpilot's driving warp pickle
|
||||
warp = getattr(op, 'model_warp', lambda *args: None)(small, cam_w, cam_h, *geometry)
|
||||
if warp is None:
|
||||
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
|
||||
elif problems := conformance(warp, FrameWarp):
|
||||
raise RuntimeError(f"the adapter's model_warp is not a warp: {', '.join(problems)}")
|
||||
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
|
||||
except Exception:
|
||||
link.close()
|
||||
raise
|
||||
|
||||
@@ -157,9 +157,9 @@ class JetlinkModelState:
|
||||
# model manager key off this flag
|
||||
self.chestnut = True
|
||||
|
||||
# a warp.Warp or the adapter's model_warp, loaded and warmed ahead: its
|
||||
# first call links the JIT, and this runs on modeld's frame thread. Frames
|
||||
# go out of its output as they are, over either transport
|
||||
# a warp.Warp, loaded and warmed ahead: its first call links the JIT, and
|
||||
# this runs on modeld's frame thread. Frames go out of its output as they
|
||||
# are, over either transport
|
||||
self.warp = warp
|
||||
|
||||
self.input_shapes = spec.input_shapes
|
||||
@@ -305,9 +305,9 @@ class JetlinkModelState:
|
||||
# over the cable: how frames go, how many never came back, and what the
|
||||
# kernel dropped on the way in (softnet), since the link came up
|
||||
drops = getattr(self.client.t, 'net_drops', lambda: None)()
|
||||
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, " +
|
||||
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, "
|
||||
f"{self.client.frames_lost} lost, net drops {drops}" if drops is not None else '')
|
||||
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; " +
|
||||
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; "
|
||||
"server gpu %.1f queue %.1f total %.1f ms; ffs maxima prepare %.1f read_wait %.1f handoff %.1f ms%s",
|
||||
self._frame_id, (frame.t1 - frame.t0) * 1e3, (frame.t2 - frame.t1) * 1e3,
|
||||
(frame.sent - frame.t2) * 1e3, (t4 - waiting_from) * 1e3,
|
||||
|
||||
@@ -161,7 +161,7 @@ class Warp:
|
||||
out = jit.captured.ret.uop.base.buffer
|
||||
self._device = out.device
|
||||
if self._device.startswith('QCOM'):
|
||||
make_coherent(out)
|
||||
_make_coherent(out)
|
||||
# the two buffers every frame passes, written in place by start()
|
||||
self._tensors = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize()
|
||||
for name, (shape, dtype, device) in specs.items()}
|
||||
@@ -191,15 +191,10 @@ class Warp:
|
||||
self._replay(self._inputs, {})
|
||||
|
||||
|
||||
def make_coherent(out) -> None:
|
||||
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back, so
|
||||
the CPU reads it through its cache once the GPU is done (see Warp). Raises
|
||||
if the kernel keeps either flag back (the allocation's record holds its
|
||||
answer): write-back memory that is not coherent reads stale frames.
|
||||
|
||||
For an adapter's model_warp too. Move a buffer before the first call of
|
||||
the JIT that writes it: that call links its address into the JIT, which
|
||||
would keep writing the memory this frees."""
|
||||
def _make_coherent(out) -> None:
|
||||
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back.
|
||||
Raises if the kernel keeps either flag back (the allocation's record holds
|
||||
its answer): write-back memory that is not coherent reads stale frames."""
|
||||
from tinygrad.device import Device
|
||||
dev = Device[out.device]
|
||||
mem = dev._gpu_alloc(out.nbytes, flags=COHERENT_WRITEBACK)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Device Params must not depend on the platform used to compile native libraries.
|
||||
if [ -f /AGNOS ] && [ -z "${PARAMS_ROOT:-}" ]; then
|
||||
export PARAMS_ROOT="/data/params"
|
||||
fi
|
||||
|
||||
export OMP_NUM_THREADS=1
|
||||
export MKL_NUM_THREADS=1
|
||||
export NUMEXPR_NUM_THREADS=1
|
||||
|
||||
Binary file not shown.
@@ -14,96 +14,21 @@ class BluePilotLateral:
|
||||
self.announced = Command()
|
||||
self.announced_ns = 0
|
||||
self.lka_counter = 0
|
||||
self.control_counter = 0
|
||||
self.control_sent_ns = 0
|
||||
self.shadow_sent_ns = 0
|
||||
self.control_echo_ns = 0
|
||||
self.shadow_echo_ns = 0
|
||||
self.control_returned_ns = 0
|
||||
self.shadow_returned_ns = 0
|
||||
self.control_echo_pending = False
|
||||
self.shadow_echo_pending = False
|
||||
self.control_return_payload = None
|
||||
self.shadow_return_payload = None
|
||||
self.pending_generation = 0
|
||||
self.announced_generation = -1
|
||||
self.sent_generation = -2
|
||||
self.steering_rejection_ns = 0
|
||||
self.apply_curvature_last = 0.0
|
||||
|
||||
def observe_can(self, can_packets, main_bus):
|
||||
# Forwarded stock traffic also returns with src|0x80. Consume one matching
|
||||
# return for our actual emitted payload; unrelated stock echoes cannot pace us.
|
||||
# Preserve return ordering here;
|
||||
# start the delay when consumed, in the controller's own clock domain.
|
||||
control_address = 0x3D6 if self.canfd else 0x3D3
|
||||
for timestamp, frames in can_packets:
|
||||
if timestamp <= 0:
|
||||
continue
|
||||
for address, data, source in frames:
|
||||
if len(data) != 8:
|
||||
continue
|
||||
if source == (main_bus | 0xC0) and address in (control_address, 0x3CA):
|
||||
if timestamp > self.steering_rejection_ns:
|
||||
self.steering_rejection_ns = timestamp
|
||||
self.host.steering_rejected = True
|
||||
continue
|
||||
if source != (main_bus | 0x80):
|
||||
continue
|
||||
if address == control_address and data == self.control_return_payload and timestamp > self.control_returned_ns:
|
||||
self.control_return_payload = None
|
||||
self.control_returned_ns = timestamp
|
||||
self.control_echo_pending = True
|
||||
elif address == 0x3CA and data == self.shadow_return_payload and timestamp > self.shadow_returned_ns:
|
||||
self.shadow_return_payload = None
|
||||
self.shadow_returned_ns = timestamp
|
||||
self.shadow_echo_pending = True
|
||||
|
||||
def step_lateral(self, CC, CS, packer, CAN, frame, now_ns):
|
||||
if now_ns <= 0:
|
||||
return []
|
||||
if self.control_echo_pending:
|
||||
self.control_echo_ns = now_ns
|
||||
self.control_echo_pending = False
|
||||
if ((self.control_sent_ns and now_ns - self.control_sent_ns < 50_000_000) or
|
||||
(self.control_echo_ns and now_ns - self.control_echo_ns < 45_000_000) or
|
||||
self.announced_generation <= self.sent_generation):
|
||||
return []
|
||||
self.pending = self.host.update(CC, CS)
|
||||
self.pending_generation += 1
|
||||
command = self.announced
|
||||
# A pending permission withdrawal is immediate; an increase waits until
|
||||
# the 33Hz side channel has announced the matching final actuator.
|
||||
fresh = 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000
|
||||
if not self.pending.active or not fresh:
|
||||
if not self.pending.active or not 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000:
|
||||
command = Command()
|
||||
self.announced = command
|
||||
if not fresh:
|
||||
# Native records the neutral actuator as zero. Do not resume an old
|
||||
# nonzero path after a new announcement; restart through the normal limits.
|
||||
self.host.controller.reset_actuator()
|
||||
self.pending = Command()
|
||||
self.control_sent_ns = now_ns
|
||||
self.sent_generation = self.announced_generation
|
||||
message = lateral(packer, CAN.main, command, canfd=self.canfd, counter=self.control_counter)
|
||||
self.control_return_payload = message[1]
|
||||
self.control_counter = (self.control_counter + 1) % 16
|
||||
return [message]
|
||||
return [lateral(packer, CAN.main, command, canfd=self.canfd, counter=frame // 5)]
|
||||
|
||||
def step_lka(self, packer, CAN, now_ns):
|
||||
if now_ns <= 0:
|
||||
return []
|
||||
if self.shadow_echo_pending:
|
||||
self.shadow_echo_ns = now_ns
|
||||
self.shadow_echo_pending = False
|
||||
if ((self.shadow_sent_ns and now_ns - self.shadow_sent_ns < 30_000_000) or
|
||||
(self.shadow_echo_ns and now_ns - self.shadow_echo_ns < 25_000_000)):
|
||||
return []
|
||||
self.announced = self.pending
|
||||
self.announced_generation = self.pending_generation
|
||||
self.announced_ns = now_ns
|
||||
self.shadow_sent_ns = now_ns
|
||||
message = lka(packer, CAN.main, self.announced.shadow_curvature, self.lka_counter)
|
||||
self.shadow_return_payload = message[1]
|
||||
self.lka_counter = (self.lka_counter + 1) % 8
|
||||
return [message]
|
||||
|
||||
@@ -51,9 +51,8 @@ class Command:
|
||||
|
||||
|
||||
class AngleController:
|
||||
def __init__(self, *, low_gain=1.0, high_gain=1.15, trim_config=None, recovery_blips=True):
|
||||
def __init__(self, *, low_gain=1.0, high_gain=1.15, trim_config=None):
|
||||
self.trim_config = TrimConfig() if trim_config is None else trim_config
|
||||
self.recovery_blips = recovery_blips
|
||||
self.trim = LaneCenterTrim()
|
||||
self.low_gain = low_gain
|
||||
self.high_gain = high_gain
|
||||
@@ -69,13 +68,10 @@ class AngleController:
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count = 0
|
||||
|
||||
def reset_actuator(self):
|
||||
def reset(self):
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.speed_last = 0.1
|
||||
|
||||
def reset(self):
|
||||
self.reset_actuator()
|
||||
self.desired_last = 0.0
|
||||
self.human_timer = 0.0
|
||||
self.press_last = False
|
||||
@@ -111,7 +107,7 @@ class AngleController:
|
||||
if i.steering_pressed:
|
||||
self.press_timer += 0.05
|
||||
else:
|
||||
if self.recovery_blips and self.press_timer >= 0.5 and self.blip_cooldown <= 0.0 and self.blip_frames <= 0 and abs(self.path_last) < 0.1:
|
||||
if self.press_timer >= 0.5 and self.blip_cooldown <= 0.0 and self.blip_frames <= 0 and abs(self.path_last) < 0.1:
|
||||
self.blip_frames = 6
|
||||
self.press_timer = 0.0
|
||||
if self.blip_frames > 0:
|
||||
@@ -131,7 +127,7 @@ class AngleController:
|
||||
entering = bool(curves) and abs(interp(base, i.time_indices, curves)) > abs(i.desired_curvature)
|
||||
factor = 1.0 if entering else interp(abs(i.desired_curvature), (0.005, 0.020), (1.0, 0.0))
|
||||
lookup = base + bounds.VLT_EXTRA * interp(i.speed, (11.176, 24.5872), (1.0, 0.0)) * factor
|
||||
predicted = interp(lookup, i.time_indices, curves) if curves else 0.0
|
||||
predicted = interp(lookup, i.time_indices, curves) if curves else i.desired_curvature
|
||||
saturated = i.limit_status >= 2 or self.path_last >= 0.5235 * 0.9 or self.path_last <= -0.5 * 0.9
|
||||
falling = abs(i.desired_curvature) < abs(self.desired_last) - 0.010
|
||||
blend = bounds.BLEND * 0.25 if not entering and (i.limit_status >= 1 or saturated or falling) else bounds.BLEND
|
||||
@@ -198,7 +194,7 @@ class AngleController:
|
||||
gap = i.desired_curvature - i.measured_curvature
|
||||
stalled = (not i.steering_pressed and not i.lane_change and i.speed > 9.0 and abs(gap) > 0.004 and
|
||||
abs(i.desired_curvature) > abs(i.measured_curvature))
|
||||
if self.recovery_blips and stalled:
|
||||
if stalled:
|
||||
if abs(kappa - requested) > 1e-9 and self.blip_cooldown <= 0.0:
|
||||
self.blip_hold += 0.05
|
||||
if self.blip_hold >= 0.5 and self.blip_count < 3 and abs(path) < 0.1:
|
||||
|
||||
@@ -4,7 +4,6 @@ Only exact final CP words derived from the existing five classic/six CANFD
|
||||
extended profiles can select angle. Combined AOL retains its original Ford profile identity.
|
||||
"""
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.pinion import base_word, configured, MASK as PINION_MASK
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs
|
||||
from opendbc.bluepilot_lateral.core.trim import LaneGeometry
|
||||
@@ -40,9 +39,7 @@ def mode_word(cp, *, selected):
|
||||
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
|
||||
valid = (safety.safetyModel == structs.CarParams.SafetyModel.ford and base_word(safety.safetyParam) == expected and
|
||||
(safety.safetyParam == expected or configured(cp)))
|
||||
return expected if valid else None
|
||||
return expected if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected else None
|
||||
|
||||
|
||||
def supported(cp):
|
||||
@@ -61,8 +58,7 @@ def select(cp, raw):
|
||||
if cp.alternativeExperience == 32:
|
||||
cp.safetyConfigs[-1].safetyParam |= ANGLE_FLAG
|
||||
else:
|
||||
metadata = cp.safetyConfigs[-1].safetyParam & PINION_MASK
|
||||
cp.safetyConfigs[-1].safetyParam = (ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)) | metadata
|
||||
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
|
||||
cp.steerActuatorDelay = 0.22
|
||||
return True
|
||||
|
||||
@@ -90,23 +86,16 @@ class Host:
|
||||
raise ValueError("BluePilot angle requires an exact admitted Ford safety word")
|
||||
low, high = (0.95, 0.95) if cp.carFingerprint in TRUCKS else ((1.0, 1.05) if cp.carFingerprint in CANFD else (1.0, 1.15))
|
||||
self.cp = cp
|
||||
from opendbc.car.ford.pinion import Measurement
|
||||
self.measurement = Measurement(cp)
|
||||
self.controller = AngleController(low_gain=low, high_gain=high)
|
||||
self.inputs = None
|
||||
self.steering_rejected = False
|
||||
self.tx_blocked = None
|
||||
|
||||
def update(self, CC, CS):
|
||||
if self.inputs is not None:
|
||||
self.inputs.update()
|
||||
self.measurement.update_parameters(getattr(self.inputs, "vehicle_parameters", None))
|
||||
snapshot = self.inputs.lateral_snapshot(CS.out.vEgoRaw, angle=True) if self.inputs is not None else None
|
||||
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
|
||||
# The host is constructed before AOL marks the final CarParams.
|
||||
# Retained AOL keeps EPS engaged through release and tracking error;
|
||||
# physical human-turn pauses and fault recovery remain independent.
|
||||
self.controller.recovery_blips = not aol
|
||||
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
|
||||
(aol and CS.out.cruiseState.available or ordinary) and CC.latActive and CS.out.canValid and
|
||||
@@ -114,10 +103,17 @@ class Host:
|
||||
(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)
|
||||
# Only an actual steering/shadow rejection invalidates its accepted path
|
||||
# and counter history. Unrelated ACC/UI rejections do not reset steering.
|
||||
rejected, self.steering_rejected = self.steering_rejected, False
|
||||
ready = ready and not rejected
|
||||
# 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:
|
||||
@@ -128,7 +124,7 @@ class Host:
|
||||
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=self.measurement.curvature(CS.out),
|
||||
active=ready, speed=CS.out.vEgoRaw, measured_curvature=-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1),
|
||||
desired_curvature=CC.actuators.curvature, steering_pressed=CS.out.steeringPressed,
|
||||
steering_angle=CS.out.steeringAngleDeg, yaw_predictions=tuple(model.orientationRate.z) if model is not None else (),
|
||||
time_indices=tuple(times), lateral_delay=delay,
|
||||
|
||||
@@ -213,8 +213,8 @@ static bool ford_bp_lateral(const CANPacket_t *msg) {
|
||||
(SAFETY_ABS(path) >= (relation_low - 2.0F)) && (SAFETY_ABS(path) <= (relation_high + 2.0F)) &&
|
||||
(SAFETY_ABS(shadow) <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN)) && (path_curvature_high <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN));
|
||||
if (speed_max > 10.0F) {
|
||||
const int measured_min = curvature_state.meas.min - (ford_pinion_enabled ? 150 : 100);
|
||||
const int measured_max = curvature_state.meas.max + (ford_pinion_enabled ? 150 : 100);
|
||||
const int measured_min = curvature_state.meas.min - 100;
|
||||
const int measured_max = curvature_state.meas.max + 100;
|
||||
const float measured_min_float = (float)measured_min;
|
||||
const float measured_max_float = (float)measured_max;
|
||||
valid &= (shadow_can >= measured_min_float) && (shadow_can <= measured_max_float);
|
||||
|
||||
@@ -26,14 +26,8 @@
|
||||
},
|
||||
"starpilot_curvature_controller": {
|
||||
"origin": "opendbc_repo/opendbc/car/ford/carcontroller.py",
|
||||
"sha256": "9838f258b7a4eef48c8c1ed4fb40d180939b86e22ac9079a734d1eabf4c0060e",
|
||||
"revision": "d2ae98eb3959c88011ed537e7cbc033d91fd3efa",
|
||||
"intentional_changes": [
|
||||
{
|
||||
"commit": "d60f2d7ada6cf6aa0f32e881aa916809686a65a6",
|
||||
"contract": "Generic CAN-FD stock-idle and same-batch mode-before-bounded-command handoff. Retains the pre-angle curvature oracle and all full-byte comparisons."
|
||||
}
|
||||
]
|
||||
"sha256": "c131d1d5f4231004e00f46968988b5d5f379ec22ef8c44d7f67ded8464344855",
|
||||
"revision": "d2ae98eb3959c88011ed537e7cbc033d91fd3efa"
|
||||
},
|
||||
"bpdev_trim": {
|
||||
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/lane_center_trim.py",
|
||||
|
||||
+4
-12
@@ -160,17 +160,13 @@ class CarController(CarControllerBase):
|
||||
else:
|
||||
self.classic_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
|
||||
previous = self.classic_lateral.curvature_last
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced or self.generic_canfd else FordLateralResult()
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced else FordLateralResult()
|
||||
bound = generic_canfd_bounded_command if self.generic_canfd else classic_bounded_command
|
||||
lateral = bound(self.classic_lateral, demanded, previous, CS.out.vEgoRaw,
|
||||
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
|
||||
self.classic_lateral_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
if self.generic_canfd:
|
||||
if self.classic_extended_announced != bool(lateral.active):
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN) if lateral.active else
|
||||
fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = bool(lateral.active)
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(create_extended_canfd_msg(
|
||||
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
|
||||
@@ -225,8 +221,7 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral_active, 0., 0., -self.apply_curvature_last, 0.))
|
||||
|
||||
# send lka msg at 33Hz
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0 and not any(
|
||||
address == 0x3CA for address, _, _ in can_sends):
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0:
|
||||
if self.CP.flags & FordFlags.LKA_STEERING:
|
||||
source_age_ns = now_nanos - CS.lkas_available_ts_nanos
|
||||
lka_active = (CC.latActive and CS.lkas_available and 0 <= source_age_ns <= 100_000_000 and
|
||||
@@ -255,11 +250,8 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.mache_extended_announced = True
|
||||
elif self.classic_lateral is not None:
|
||||
if self.generic_canfd and not self.classic_extended_announced:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
else:
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
else:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
|
||||
|
||||
@@ -57,27 +57,6 @@ class TestAngleCore(unittest.TestCase):
|
||||
self.assertTrue(resumed.active)
|
||||
self.assertLess(abs(resumed.path_angle), .02)
|
||||
|
||||
def test_retained_aol_keeps_active_after_release_and_tracking_error(self):
|
||||
for stalled in (False, True):
|
||||
core = AngleController(recovery_blips=False)
|
||||
for tick in range(100):
|
||||
inputs = Inputs(True, 15., 0., .006 if stalled else .001,
|
||||
steering_pressed=not stalled and tick < 20)
|
||||
result = core.step(inputs)
|
||||
self.assertTrue(result.active, (stalled, tick))
|
||||
self.assertLessEqual(abs(result.path_angle / (15. * limits.GAIN_MIN)), limits.LATERAL_ACCEL / 15. ** 2)
|
||||
self.assertEqual(core.blip_frames, 0)
|
||||
|
||||
def test_retained_aol_preserves_human_turn_and_invalid_input_neutral(self):
|
||||
core = AngleController(recovery_blips=False)
|
||||
for _ in range(61):
|
||||
result = core.step(Inputs(True, 15., 0., .001, steering_pressed=True, steering_angle=60.))
|
||||
self.assertEqual(result, Command())
|
||||
self.assertEqual(core.step(Inputs(True, 15., 0., math.nan)), Command())
|
||||
self.assertEqual(core.step(Inputs(False, 15., 0., .001)), Command())
|
||||
# Infeasible measured-curvature and actuator envelopes must still go neutral.
|
||||
self.assertEqual(core.step(Inputs(True, 15., .1, .001)), Command())
|
||||
|
||||
def test_pinned_bp7_pure_angle_and_actual_control_bytes_with_documented_limits(self):
|
||||
builders = reference.builders()
|
||||
bus = SimpleNamespace(main=0)
|
||||
@@ -192,7 +171,7 @@ class TestAngleTrim(unittest.TestCase):
|
||||
for changes in ({"active": False}, {"lane_change": True}, {"steering_pressed": True}):
|
||||
trim = LaneCenterTrim()
|
||||
trim.update(0., 15., original, config, active=True, lane_change=False)
|
||||
flags = {"active": True, "lane_change": False, "steering_pressed": False}
|
||||
flags = dict(active=True, lane_change=False, steering_pressed=False)
|
||||
flags.update(changes)
|
||||
self.assertEqual(trim.update(.001, 15., original, config, **flags), .001)
|
||||
self.assertEqual(trim.correction, 0.)
|
||||
@@ -232,19 +211,3 @@ class TestAngleTrim(unittest.TestCase):
|
||||
else:
|
||||
previous = 0.
|
||||
self.assertEqual(core.trim.correction, 0.)
|
||||
|
||||
|
||||
class TestAngleInputParity(unittest.TestCase):
|
||||
def test_missing_model_fallback_matches_pinned_bp7(self):
|
||||
for identity in sorted(CLASSIC | CANFD):
|
||||
gains = (.95, .95) if identity in TRUCKS else ((1., 1.05) if identity in CANFD else (1., 1.15))
|
||||
for speed in (5., 8.):
|
||||
for direction in (-1, 1):
|
||||
adapted = AngleController(low_gain=gains[0], high_gain=gains[1])
|
||||
upstream = reference.Angle(identity)
|
||||
for _ in range(20):
|
||||
inputs = Inputs(True, speed, direction * .001, direction * .001)
|
||||
command, expected = adapted.step(inputs), upstream.step(inputs)
|
||||
self.assertTrue(command.active)
|
||||
self.assertAlmostEqual(command.path_angle, expected.path_angle, places=12)
|
||||
self.assertEqual(command.precision, expected.precision_type)
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
"""Mach-E route 0000000d: refresh before publication; neutral restarts from zero."""
|
||||
import pytest
|
||||
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.car import structs, Bus
|
||||
from opendbc.car.ford.values import CAR
|
||||
from opendbc.car.ford.carcontroller import CarController
|
||||
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs, Command
|
||||
|
||||
|
||||
class CoreHost:
|
||||
def __init__(self):
|
||||
self.controller = AngleController(low_gain=1., high_gain=1.05, recovery_blips=False)
|
||||
self.steering_rejected = False
|
||||
self.steering_pressed = False
|
||||
self.steering_angle = 0.
|
||||
|
||||
def update(self, cc, cs):
|
||||
active = cc.latActive and not self.steering_rejected
|
||||
self.steering_rejected = False
|
||||
return self.controller.step(Inputs(active, 32.6, 0., .0012, steering_pressed=self.steering_pressed, steering_angle=self.steering_angle))
|
||||
|
||||
|
||||
def setup(canfd=True):
|
||||
cp = structs.CarParams(brand='ford', carFingerprint=CAR.FORD_MUSTANG_MACH_E_MK1 if canfd else CAR.FORD_FOCUS_MK4,
|
||||
flags=3 if canfd else 2, pcmCruise=True, alternativeExperience=32,
|
||||
transmissionType=structs.CarParams.TransmissionType.automatic,
|
||||
steerControlType=structs.CarParams.SteerControlType.angle,
|
||||
openpilotLongitudinalControl=True)
|
||||
cp.safetyConfigs = [structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.ford, safetyParam=147 if canfd else 161)]
|
||||
owner = CarController({Bus.pt:'ford_lincoln_base_pt'}, cp)
|
||||
owner.bp_lat.host = CoreHost()
|
||||
cc = structs.CarControl(latActive=True)
|
||||
state = structs.CarState(vEgo=32.6, vEgoRaw=32.6, canValid=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive)
|
||||
state.cruiseState.available = True
|
||||
cs = SimpleNamespace(out=state.as_reader(), buttons_stock_values=defaultdict(int),
|
||||
acc_tja_status_stock_values=defaultdict(int), lkas_status_stock_values=defaultdict(int))
|
||||
return owner, cc.as_reader(), cs
|
||||
|
||||
|
||||
def control_active(message):
|
||||
return ((message[1][0] >> 4) if message[0] == 0x3D6 else (message[1][4] >> 2)) & 7
|
||||
|
||||
|
||||
def prime(owner, cc, cs):
|
||||
for now in range(1_000_000_000, 2_000_000_000, 10_000_000):
|
||||
owner.update(cc, cs, now)
|
||||
assert owner.bp_lat.pending.active
|
||||
assert abs(owner.bp_lat.host.controller.path_last) > .015
|
||||
|
||||
|
||||
def test_due_shadow_precedes_control_after_recorded_72ms_gap():
|
||||
owner, cc, cs = setup()
|
||||
prime(owner, cc, cs)
|
||||
bp = owner.bp_lat
|
||||
now = max(bp.shadow_sent_ns, bp.control_sent_ns) + 72_000_000
|
||||
_, messages = owner.update(cc, cs, now)
|
||||
angle = [m for m in messages if m[0] in (0x3CA, 0x3D6)]
|
||||
assert [m[0] for m in angle] == [0x3CA, 0x3D6]
|
||||
assert control_active(angle[-1]) == 1
|
||||
assert bp.announced_ns == now
|
||||
|
||||
|
||||
def test_unrefreshable_stale_lease_neutral_clears_pending_actuator_history():
|
||||
owner, cc, cs = setup()
|
||||
prime(owner, cc, cs)
|
||||
bp = owner.bp_lat
|
||||
now = max(bp.shadow_sent_ns, bp.control_sent_ns) + 72_000_000
|
||||
# A newly consumed late shadow echo still enforces its pacing delay.
|
||||
bp.shadow_echo_pending = True
|
||||
_, messages = owner.update(cc, cs, now)
|
||||
angle = [m for m in messages if m[0] in (0x3CA, 0x3D6)]
|
||||
assert [m[0] for m in angle] == [0x3D6]
|
||||
assert control_active(angle[0]) == 0
|
||||
assert bp.pending == Command()
|
||||
assert bp.host.controller.path_last == 0.
|
||||
# A subsequent announcement cannot revive the old nonzero actuator.
|
||||
seen = []
|
||||
for t in range(now + 10_000_000, now + 300_000_000, 10_000_000):
|
||||
_, messages = owner.update(cc, cs, t)
|
||||
seen.extend(m for m in messages if m[0] == 0x3D6 and control_active(m))
|
||||
assert seen
|
||||
sig = owner.packer.dbc.name_to_msg['LateralMotionControl2'].sigs['LatCtlPath_An_Actl']
|
||||
first_path = sig.get_raw_value(seen[0][1]) * sig.factor + sig.offset
|
||||
assert abs(first_path) <= .005
|
||||
|
||||
|
||||
@pytest.mark.parametrize('canfd, word', [(True, 147), (False, 161)])
|
||||
def test_aol_native_accepts_due_refresh_and_stale_restart_without_relaxation(canfd, word):
|
||||
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
native = TestFordAolDriverIntent()
|
||||
native.setUp()
|
||||
native.reset(word)
|
||||
native.arm()
|
||||
native.pump(30, speed=32.6, mask=1)
|
||||
owner, cc, cs = setup(canfd)
|
||||
active = 0
|
||||
try:
|
||||
for tick in range(200):
|
||||
native.pump(1, speed=32.6, mask=1)
|
||||
now = (1_000_000 + native.tick * 10_000) * 1000
|
||||
if tick in (110, 150):
|
||||
# No host update for 70ms, but native receives fresh vehicle sources.
|
||||
native.pump(7, speed=32.6, mask=1)
|
||||
now = (1_000_000 + native.tick * 10_000) * 1000
|
||||
if tick == 150:
|
||||
owner.bp_lat.shadow_echo_pending = True
|
||||
_, messages = owner.update(cc, cs, now)
|
||||
for address, data, bus in messages:
|
||||
if address not in (0x3CA, 0x3D6, 0x3D3):
|
||||
continue
|
||||
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)), (tick, hex(address), data.hex())
|
||||
active += address in (0x3D6, 0x3D3) and bool(control_active((address, data, bus)))
|
||||
assert active > 20
|
||||
finally:
|
||||
native.tearDown()
|
||||
|
||||
|
||||
def test_rejected_shadow_is_not_an_ack_and_withdraws_next_control():
|
||||
owner, cc, cs = setup()
|
||||
prime(owner, cc, cs)
|
||||
bp = owner.bp_lat
|
||||
now = max(bp.shadow_sent_ns, bp.control_sent_ns) + 72_000_000
|
||||
payload = bp.shadow_return_payload
|
||||
bp.observe_can([(now, [(0x3CA, payload, 0xC0)])], 0)
|
||||
assert bp.host.steering_rejected
|
||||
assert not bp.shadow_echo_pending
|
||||
_, messages = owner.update(cc, cs, now)
|
||||
controls = [m for m in messages if m[0] == 0x3D6]
|
||||
assert controls and control_active(controls[0]) == 0
|
||||
assert bp.host.controller.path_last == 0.
|
||||
|
||||
|
||||
def test_unrelated_or_out_of_order_returns_do_not_refresh_shadow_time():
|
||||
owner, cc, cs = setup()
|
||||
prime(owner, cc, cs)
|
||||
bp = owner.bp_lat
|
||||
sent = bp.shadow_sent_ns
|
||||
payload = bp.shadow_return_payload
|
||||
bp.shadow_returned_ns = sent + 10_000_000
|
||||
bp.observe_can([(sent, [(0x3CA, payload, 0x80)])], 0)
|
||||
bp.observe_can([(sent + 20_000_000, [(0x3CA, bytes(8), 0x80)])], 0)
|
||||
assert not bp.shadow_echo_pending
|
||||
assert bp.announced_ns == sent
|
||||
|
||||
|
||||
def test_permission_withdrawal_does_not_publish_active_even_with_due_shadow():
|
||||
owner, cc, cs = setup()
|
||||
prime(owner, cc, cs)
|
||||
now = max(owner.bp_lat.shadow_sent_ns, owner.bp_lat.control_sent_ns) + 72_000_000
|
||||
_, messages = owner.update(structs.CarControl().as_reader(), cs, now)
|
||||
controls = [m for m in messages if m[0] == 0x3D6]
|
||||
assert controls and control_active(controls[0]) == 0
|
||||
assert owner.bp_lat.pending == Command()
|
||||
|
||||
|
||||
def test_stale_transport_reset_preserves_continuous_manual_turn_pause():
|
||||
owner, cc, cs = setup()
|
||||
prime(owner, cc, cs)
|
||||
host = owner.bp_lat.host
|
||||
host.steering_pressed = True
|
||||
host.steering_angle = 60.
|
||||
for _ in range(70):
|
||||
command = host.update(cc, cs)
|
||||
assert not command.active
|
||||
assert host.controller.human_timer >= 3.
|
||||
now = max(owner.bp_lat.shadow_sent_ns, owner.bp_lat.control_sent_ns) + 72_000_000
|
||||
owner.bp_lat.shadow_echo_pending = True
|
||||
_, messages = owner.update(cc, cs, now)
|
||||
assert not any(control_active(m) for m in messages if m[0] == 0x3D6)
|
||||
assert host.controller.human_timer >= 3.
|
||||
assert host.controller.press_last and host.controller.preturned
|
||||
for t in range(now + 10_000_000, now + 500_000_000, 10_000_000):
|
||||
_, messages = owner.update(cc, cs, t)
|
||||
assert not any(control_active(m) for m in messages if m[0] == 0x3D6)
|
||||
@@ -1,24 +1,19 @@
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
PyObject *names[4];
|
||||
} ExtractState;
|
||||
|
||||
static PyObject *get_raw_value(PyObject *self, PyObject *const *args, Py_ssize_t nargs) {
|
||||
static PyObject *get_raw_value(PyObject *self, PyObject *args) {
|
||||
ExtractState *state = PyModule_GetState(self);
|
||||
PyObject *signal, *payload, *expected;
|
||||
PyObject *msb_obj = NULL, *lsb_obj = NULL, *size_obj = NULL, *little_obj = NULL;
|
||||
const unsigned char *data;
|
||||
Py_ssize_t length;
|
||||
long msb, lsb, size, little, first, last;
|
||||
if (nargs != 3) {
|
||||
PyErr_Format(PyExc_TypeError, "get_raw_value expected 3 arguments, got %zd", nargs);
|
||||
return NULL;
|
||||
}
|
||||
signal = args[0]; payload = args[1]; expected = args[2];
|
||||
if (!PyArg_UnpackTuple(args, "get_raw_value", 3, 3, &signal, &payload, &expected)) return NULL;
|
||||
if (state == NULL || state->names[0] == NULL || state->names[1] == NULL ||
|
||||
state->names[2] == NULL || state->names[3] == NULL) goto unsupported;
|
||||
if (!PyType_Check(expected) || Py_TYPE(signal) != (PyTypeObject *)expected) goto unsupported;
|
||||
@@ -59,27 +54,21 @@ static PyObject *get_raw_value(PyObject *self, PyObject *const *args, Py_ssize_t
|
||||
if (size != 8 * (last - first) + msb % 8 - lsb % 8 + 1) goto unsupported;
|
||||
}
|
||||
if (first < 0 || first > last || last >= length) goto unsupported;
|
||||
/* Contiguous signals need at most nine bytes, including an unaligned
|
||||
64-bit field. memcpy permits unaligned payloads without aliasing UB. */
|
||||
const unsigned count = (unsigned)(last - first + 1);
|
||||
const unsigned shift = (unsigned)(lsb % 8);
|
||||
uint64_t raw = 0;
|
||||
memcpy(&raw, data + first, count > 8U ? 8U : count);
|
||||
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
raw = __builtin_bswap64(raw);
|
||||
#elif __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
|
||||
#error Unsupported byte order
|
||||
#endif
|
||||
uint64_t result;
|
||||
if (little) {
|
||||
result = raw >> shift;
|
||||
if (count == 9U) result |= (uint64_t)data[last] << (64U - shift);
|
||||
} else {
|
||||
raw = __builtin_bswap64(raw);
|
||||
if (count == 9U) result = (raw << (8U - shift)) | ((uint64_t)data[last] >> shift);
|
||||
else result = (raw >> (8U * (8U - count))) >> shift;
|
||||
uint64_t result = 0;
|
||||
long remaining = size;
|
||||
for (long byte = msb / 8; remaining > 0; byte += little ? -1 : 1) {
|
||||
if (byte < 0 || byte >= length) goto unsupported;
|
||||
const long low = lsb / 8 == byte ? lsb : byte * 8;
|
||||
const long high = msb / 8 == byte ? msb : (byte + 1) * 8 - 1;
|
||||
const long width = high - low + 1;
|
||||
const long bit_shift = low - byte * 8;
|
||||
const long result_shift = remaining - width;
|
||||
if (width <= 0 || width > 8 || bit_shift < 0 || bit_shift > 7 ||
|
||||
result_shift < 0 || result_shift > 63) goto unsupported;
|
||||
const unsigned int fragment = (data[byte] >> bit_shift) & ((1U << width) - 1U);
|
||||
result |= (uint64_t)fragment << result_shift;
|
||||
remaining -= width;
|
||||
}
|
||||
if (size < 64) result &= (UINT64_C(1) << size) - 1U;
|
||||
Py_DECREF(msb_obj);
|
||||
Py_DECREF(lsb_obj);
|
||||
Py_DECREF(size_obj);
|
||||
@@ -97,7 +86,7 @@ unsupported:
|
||||
}
|
||||
|
||||
static PyMethodDef methods[] = {
|
||||
{"get_raw_value", (PyCFunction)(void(*)(void))get_raw_value, METH_FASTCALL, "Extract an exact complete <=64-bit CAN signal, or NotImplemented."},
|
||||
{"get_raw_value", get_raw_value, METH_VARARGS, "Extract an exact complete <=64-bit CAN signal, or NotImplemented."},
|
||||
{NULL, NULL, 0, NULL},
|
||||
};
|
||||
static int module_traverse(PyObject *self, visitproc visit, void *arg) {
|
||||
|
||||
Binary file not shown.
@@ -112,18 +112,10 @@ class VLDict(dict):
|
||||
|
||||
|
||||
class CANParser:
|
||||
def __init__(self, dbc_name: str, messages: Sequence[tuple[str | int, float]], bus: int, *, signals: dict[str | int, Sequence[str]] | None = None):
|
||||
def __init__(self, dbc_name: str, messages: Sequence[tuple[str | int, float]], bus: int):
|
||||
self.dbc_name: str = dbc_name
|
||||
self.bus: int = bus
|
||||
self.dbc = DBC(dbc_name)
|
||||
self._signal_selection: dict[int, frozenset[str]] = {}
|
||||
for key, names in (signals or {}).items():
|
||||
message = self.dbc.addr_to_msg.get(int(key)) if isinstance(key, numbers.Number) else self.dbc.name_to_msg.get(key)
|
||||
if message is None or not set(names).issubset(message.sigs):
|
||||
raise ValueError(f"Unknown signal selection for {key!r} in DBC {dbc_name}")
|
||||
if message.address in self._signal_selection:
|
||||
raise ValueError(f"Duplicate signal selection for {key!r}")
|
||||
self._signal_selection[message.address] = frozenset(names)
|
||||
|
||||
self.vl: dict[int | str, dict[str, float]] = VLDict(self)
|
||||
self.vl_all: dict[int | str, dict[str, list[float]]] = {}
|
||||
@@ -156,13 +148,7 @@ class CANParser:
|
||||
assert msg.address not in self.addresses
|
||||
|
||||
self.addresses.add(msg.address)
|
||||
selected = self._signal_selection.get(msg.address)
|
||||
# Projection only: counters and checksum fields still use the original
|
||||
# decoder, validation, rejection and freshness path on every message.
|
||||
decoded_signals = [sig for sig in msg.sigs.values() if selected is None or sig.name in selected or
|
||||
sig.type != 0 or sig.calc_checksum is not None or
|
||||
'COUNTER' in sig.name or 'CHECKSUM' in sig.name]
|
||||
signal_names = [sig.name for sig in decoded_signals]
|
||||
signal_names = list(msg.sigs.keys())
|
||||
signals_dict = {s: 0.0 for s in signal_names}
|
||||
dict.__setitem__(self.vl, msg.address, signals_dict)
|
||||
dict.__setitem__(self.vl, msg.name, signals_dict)
|
||||
@@ -175,7 +161,7 @@ class CANParser:
|
||||
address=msg.address,
|
||||
name=msg.name,
|
||||
size=msg.size,
|
||||
signals=decoded_signals,
|
||||
signals=list(msg.sigs.values()),
|
||||
ignore_alive=freq is not None and math.isnan(freq),
|
||||
)
|
||||
if freq is not None and freq > 0:
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.can.tests import TEST_DBC
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dbc,message,field', [
|
||||
(TEST_DBC, 'CAN_FD_MESSAGE', 'SIGNED'),
|
||||
('honda_civic_touring_2016_can_generated', 'STEERING_CONTROL', 'STEER_TORQUE'),
|
||||
])
|
||||
def test_selected_values_preserve_integrity_and_freshness(dbc, message, field):
|
||||
full = CANParser(dbc, [(message, 100)], 0)
|
||||
selected = CANParser(dbc, [(message, 100)], 0, signals={message: [field]})
|
||||
packer = CANPacker(dbc)
|
||||
frames = [packer.make_can_msg(message, 0, {field: 12}) for _ in range(12)]
|
||||
frames += [frames[-1]] * 8 # Repeated counters must still fail.
|
||||
damaged = bytearray(frames[-1][1])
|
||||
damaged[0] ^= 0x80
|
||||
frames += [(frames[-1][0], bytes(damaged), 0)] * 8
|
||||
for index, frame in enumerate(frames):
|
||||
sample = [(1_000_000_000 + index * 10_000_000, [frame])]
|
||||
assert selected.update(sample) == full.update(sample)
|
||||
assert selected.can_valid == full.can_valid
|
||||
assert selected.bus_timeout == full.bus_timeout
|
||||
for key in selected.vl[message]:
|
||||
assert selected.vl[message][key] == full.vl[message][key]
|
||||
assert selected.vl_all[message][key] == full.vl_all[message][key]
|
||||
assert selected.ts_nanos[message][key] == full.ts_nanos[message][key]
|
||||
for addr, state in selected.message_states.items():
|
||||
reference = full.message_states[addr]
|
||||
assert (state.counter, state.counter_fail, list(state.timestamps)) == (reference.counter, reference.counter_fail, list(reference.timestamps))
|
||||
for _ in range(5):
|
||||
selected.update([(9_000_000_000, [])])
|
||||
full.update([(9_000_000_000, [])])
|
||||
selected_valid, full_valid = selected.can_valid, full.can_valid
|
||||
assert selected_valid == full_valid
|
||||
assert not selected_valid and not full_valid
|
||||
assert selected.bus_timeout and full.bus_timeout
|
||||
for signal in full.message_states[frames[0][0]].signals:
|
||||
if signal.type or signal.calc_checksum:
|
||||
assert signal.name in selected.vl[message]
|
||||
|
||||
|
||||
def test_unknown_projection_is_rejected_and_lazy_selection_is_honored():
|
||||
with pytest.raises(ValueError, match='Unknown signal'):
|
||||
CANParser(TEST_DBC, [], 0, signals={'CAN_FD_MESSAGE': ['not_a_signal']})
|
||||
with pytest.raises(ValueError, match='Unknown signal'):
|
||||
CANParser(TEST_DBC, [], 0, signals={'not_a_message': []})
|
||||
parser = CANParser(TEST_DBC, [], 0, signals={'CAN_FD_MESSAGE': ['SIGNED']})
|
||||
assert 'SIGNED' in parser.vl['CAN_FD_MESSAGE']
|
||||
assert 'COUNTER' in parser.vl['CAN_FD_MESSAGE']
|
||||
assert parser.message_states[245].signals
|
||||
@@ -10,9 +10,8 @@ from opendbc.car.fw_versions import (ObdCallback, get_fw_versions, get_fw_versio
|
||||
get_present_ecus, match_fw_to_car)
|
||||
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, normal_bus0_topology as gm_normal_bus0_topology
|
||||
from opendbc.car.ford.discovery import (normal_bus0_selection as ford_normal_bus0_selection,
|
||||
normal_bus0_queries as ford_normal_bus0_queries, normal_bus_query_brand)
|
||||
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
|
||||
@@ -45,15 +44,13 @@ interface_names = _get_interface_names()
|
||||
interfaces = load_interfaces(interface_names)
|
||||
|
||||
|
||||
def can_fingerprint(can_recv: CanRecvCallable, *, vin: str = VIN_UNKNOWN,
|
||||
camera_cruise_messages: set[bytes] | None = None) -> tuple[str | None, dict[int, dict]]:
|
||||
def can_fingerprint(can_recv: CanRecvCallable) -> tuple[str | None, dict[int, dict]]:
|
||||
finger = gen_empty_fingerprint()
|
||||
candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
|
||||
frame = 0
|
||||
car_fingerprint = None
|
||||
done = False
|
||||
if camera_cruise_messages is None:
|
||||
camera_cruise_messages = set()
|
||||
camera_cruise_states = set()
|
||||
|
||||
while not done:
|
||||
# can_recv(wait_for_one=True) may return zero or multiple packets, so we increment frame for each one we receive
|
||||
@@ -62,7 +59,7 @@ def can_fingerprint(can_recv: CanRecvCallable, *, vin: str = VIN_UNKNOWN,
|
||||
for can in can_packet:
|
||||
# Stock camera only: host echoes and malformed DLCs are not capability evidence.
|
||||
if can.src == 2 and can.address == 0x370 and len(can.dat) == 6:
|
||||
camera_cruise_messages.add(bytes(can.dat))
|
||||
camera_cruise_states.add(can.dat[1] & 0x7)
|
||||
# The fingerprint dict is generated for all buses, this way the car interface
|
||||
# can use it to detect a (valid) multipanda setup and initialize accordingly
|
||||
if can.src < 128:
|
||||
@@ -90,7 +87,7 @@ def can_fingerprint(can_recv: CanRecvCallable, *, vin: str = VIN_UNKNOWN,
|
||||
frame += 1
|
||||
|
||||
car_fingerprint = automatic_volt_candidate(car_fingerprint, finger, candidate_cars)
|
||||
return automatic_bolt_candidate(car_fingerprint, finger, camera_cruise_messages, vin), finger
|
||||
return automatic_bolt_candidate(car_fingerprint, finger, camera_cruise_states), finger
|
||||
|
||||
|
||||
def _gm_fw_uses_normal_bus0() -> bool:
|
||||
@@ -146,18 +143,15 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
|
||||
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_brand := normal_bus_query_brand(
|
||||
can_recv, normal_bus_topology,
|
||||
gm_probe=gm_normal_bus0_topology if _gm_fw_uses_normal_bus0() else None)) is not None:
|
||||
elif normal_bus_topology(can_recv):
|
||||
|
||||
set_obd_multiplexing(False)
|
||||
warmup_remaining = 0.1 - (time.monotonic() - start_time)
|
||||
if warmup_remaining > 0:
|
||||
time.sleep(warmup_remaining)
|
||||
vin_buses = (0,) if normal_brand in ('ford', 'gm') else (0, 1)
|
||||
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, vin_buses)
|
||||
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0, 1))
|
||||
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing,
|
||||
query_brand=normal_brand, normal_bus_only=True)
|
||||
query_brand='hyundai', normal_bus_only=True)
|
||||
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
|
||||
else:
|
||||
# enable OBD multiplexing for VIN query
|
||||
@@ -188,8 +182,7 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
|
||||
# CAN fingerprint
|
||||
# drain CAN socket so we get the latest messages
|
||||
can_recv()
|
||||
camera_cruise_messages: set[bytes] = set()
|
||||
car_fingerprint, finger = can_fingerprint(can_recv, vin=vin, camera_cruise_messages=camera_cruise_messages)
|
||||
car_fingerprint, finger = can_fingerprint(can_recv)
|
||||
|
||||
exact_match = True
|
||||
source = CarParams.FingerprintSource.can
|
||||
@@ -200,11 +193,6 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
|
||||
source = CarParams.FingerprintSource.fw
|
||||
exact_match = exact_fw_match
|
||||
|
||||
# Firmware catalogues can use the same broad Bolt aliases as legacy CAN.
|
||||
# Refine after both automatic sources, before the explicit override below.
|
||||
if not fixed_fingerprint:
|
||||
car_fingerprint = automatic_bolt_candidate(car_fingerprint, finger, camera_cruise_messages, vin)
|
||||
|
||||
if fixed_fingerprint:
|
||||
car_fingerprint = fixed_fingerprint
|
||||
source = CarParams.FingerprintSource.fixed
|
||||
@@ -251,8 +239,7 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
|
||||
candidate = "MOCK"
|
||||
|
||||
CarInterface = interfaces[candidate]
|
||||
params_options = {'auto_pedal': False} if source == CarParams.FingerprintSource.fixed and candidate in interface_names['gm'] else {}
|
||||
CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, alpha_long_allowed, is_release, docs=False, **params_options)
|
||||
CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, alpha_long_allowed, is_release, docs=False)
|
||||
CP.carVin = vin
|
||||
CP.carFw = car_fw
|
||||
CP.fingerprintSource = source
|
||||
|
||||
@@ -12,9 +12,6 @@ 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)
|
||||
from opendbc.car.ford.pinion import ENABLE, SHIFT, PROFILES, base_word, configured
|
||||
AOL_WORDS |= frozenset(word | ENABLE | index << SHIFT for index, words in PROFILES.items()
|
||||
for word in tuple(AOL_WORDS) if (word & ~MANUAL) in words)
|
||||
|
||||
|
||||
def aol_safety_config(cp):
|
||||
@@ -36,10 +33,6 @@ def base_safety_param(cp):
|
||||
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
|
||||
return -1
|
||||
word = int(owner.safetyParam)
|
||||
normalized = base_word(word)
|
||||
if normalized != word and not configured(cp):
|
||||
return -1
|
||||
word = normalized
|
||||
if (word & MANUAL and not manual) or (manual and cp.alternativeExperience == AOL_EXPERIENCE and not word & MANUAL):
|
||||
return -1
|
||||
if word & ANGLE:
|
||||
|
||||
@@ -16,7 +16,7 @@ from opendbc.car.ford.generic_canfd_can import create_extended_canfd_msg
|
||||
from opendbc.car.ford.generic_canfd_lateral import (
|
||||
GenericCanfdLateralController, qualified as generic_canfd_qualified, bounded_command as generic_canfd_bounded_command,
|
||||
)
|
||||
from opendbc.car.ford.mache_lateral import MachELateralController, FordLateralResult, bounded_command, path_speed_window_supported, qualified as mache_qualified
|
||||
from opendbc.car.ford.mache_lateral import MachELateralController, FordLateralResult, bounded_command, qualified as mache_qualified
|
||||
from opendbc.car.ford.new_port_curvature import create_controller as create_new_port_curvature, bounded_command as new_port_bounded_command
|
||||
from opendbc.car.ford.values import CarControllerParams, FordFlags, FordSafetyFlags, CAR
|
||||
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
|
||||
@@ -142,13 +142,10 @@ class CarController(CarControllerBase):
|
||||
|
||||
### lateral control ###
|
||||
# send steer msg at 20Hz
|
||||
if self.bp_lat is not None:
|
||||
# Publish a due shadow before its dependent actuator, not after a stale-lease withdrawal.
|
||||
can_sends += self.bp_lat.step_lka(self.packer, self.CAN, now_nanos)
|
||||
lateral_sends = self.bp_lat.step_lateral(CC, CS, self.packer, self.CAN, self.frame, now_nanos)
|
||||
can_sends += lateral_sends
|
||||
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(lateral_sends and self.bp_lat.announced.active and self.bp_lat.pending.active)
|
||||
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.,
|
||||
@@ -165,8 +162,7 @@ class CarController(CarControllerBase):
|
||||
previous = self.mache_lateral.curvature_last
|
||||
previous_path_angle = self.mache_lateral.path_angle_last
|
||||
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,
|
||||
getattr(CS, "bluepilot_speed_window", None) if path_speed_window_supported(self.CP) else None)
|
||||
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
|
||||
@@ -181,20 +177,13 @@ class CarController(CarControllerBase):
|
||||
else:
|
||||
self.classic_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
|
||||
previous = self.classic_lateral.curvature_last
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced or self.generic_canfd else FordLateralResult()
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced else FordLateralResult()
|
||||
bound = generic_canfd_bounded_command if self.generic_canfd else classic_bounded_command
|
||||
lateral = bound(self.classic_lateral, demanded, previous, CS.out.vEgoRaw,
|
||||
self.classic_lateral._current_curvature(CS))
|
||||
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
|
||||
self.classic_lateral_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
if self.generic_canfd:
|
||||
# Stay in the stock interface while inactive. Announce a mode change in
|
||||
# the same batch, before the corresponding bounded steering command.
|
||||
if self.classic_extended_announced != bool(lateral.active):
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN) if lateral.active else
|
||||
fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = bool(lateral.active)
|
||||
self.mache_ui_owned |= bool(lateral.active)
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(create_extended_canfd_msg(
|
||||
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
|
||||
@@ -210,7 +199,7 @@ class CarController(CarControllerBase):
|
||||
previous = self.new_port_curvature.curvature_last
|
||||
demanded = self.new_port_curvature.update(CC, CS, actuators)
|
||||
lateral = new_port_bounded_command(self.new_port_curvature, demanded, previous, CS.out.vEgoRaw,
|
||||
self.new_port_curvature._current_curvature(CS))
|
||||
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
|
||||
self.new_port_curvature_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
@@ -265,8 +254,7 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral_active, 0., 0., -self.apply_curvature_last, 0.))
|
||||
|
||||
# send lka msg at 33Hz
|
||||
if (self.bp_lat is not None or (self.frame % CarControllerParams.LKA_STEP) == 0) and not any(
|
||||
address == 0x3CA for address, _, _ in can_sends):
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0:
|
||||
if self.bp_lat is not None:
|
||||
can_sends += self.bp_lat.step_lka(self.packer, self.CAN, now_nanos)
|
||||
elif self.CP.flags & FordFlags.LKA_STEERING:
|
||||
@@ -297,11 +285,8 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.mache_extended_announced = True
|
||||
elif self.classic_lateral is not None:
|
||||
if self.generic_canfd and not self.classic_extended_announced:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
else:
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
else:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
|
||||
@@ -353,11 +338,9 @@ class CarController(CarControllerBase):
|
||||
### ui ###
|
||||
# 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_owned_ui = qualified_aol(self.CP, marked_only=True) and any(
|
||||
s.safetyModel == structs.CarParams.SafetyModel.ford and (s.safetyParam & ~(1 | 128 | 256)) in (18, 66)
|
||||
for s in self.CP.safetyConfigs)
|
||||
ui_enabled = CC.latActive and (not aol_owned_ui or self.mache_ui_owned)
|
||||
if not aol_owned_ui or ui_enabled:
|
||||
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:
|
||||
|
||||
@@ -6,7 +6,6 @@ from opendbc.can.dbc import DBC as CANDBC
|
||||
from opendbc.car import Bus, create_button_events, structs
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.ford.fordcan import CanBus
|
||||
from opendbc.car.ford.mache_lateral import path_speed_window_supported
|
||||
from opendbc.car.ford.stock_cruise import FordStockCruiseButton
|
||||
from opendbc.car.ford.values import CAR, DBC, CarControllerParams, FordFlags
|
||||
from opendbc.car.interfaces import CarStateBase
|
||||
@@ -41,8 +40,7 @@ class CarState(CarStateBase):
|
||||
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 or
|
||||
path_speed_window_supported(CP)) else 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):
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Normal-bus discovery for explicitly selected Ford CAN-FD vehicles."""
|
||||
import time
|
||||
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
|
||||
|
||||
@@ -36,53 +34,3 @@ def normal_bus0_queries():
|
||||
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())
|
||||
|
||||
|
||||
# Normal harness PT/chassis and stock-camera messages. This selects only a
|
||||
# diagnostic query family; live VIN, ECU responses and fingerprinting still
|
||||
# determine the vehicle and its control configuration.
|
||||
NORMAL_BUS0_LAYOUT = {(0, 0x82): 8, (0, 0x165): 8, (0, 0x204): 8,
|
||||
(0, 0x217): 8, (2, 0x3CA): 8, (2, 0x3D6): 8}
|
||||
|
||||
|
||||
def normal_bus_query_brand(can_recv, hyundai_probe, *, gm_probe=None, clock=time.monotonic):
|
||||
# Share the existing bounded passive HKG read, rather than consuming a second
|
||||
# startup window or enabling the OBD mux to find out whether this is a Ford.
|
||||
received = {}
|
||||
gm_received = {}
|
||||
gm_malformed = False
|
||||
malformed = False
|
||||
reads = 0
|
||||
deadline = clock() + .12
|
||||
|
||||
def observe(**kwargs):
|
||||
nonlocal malformed, gm_malformed, reads
|
||||
reads += 1
|
||||
packets = can_recv(**kwargs)
|
||||
for packet in packets:
|
||||
for frame in packet:
|
||||
key = (frame.src, frame.address)
|
||||
if gm_probe is not None:
|
||||
bus = gm_received.setdefault(frame.src, {})
|
||||
length = len(frame.dat)
|
||||
gm_malformed |= frame.address in bus and bus[frame.address] != length
|
||||
bus[frame.address] = length
|
||||
if key in NORMAL_BUS0_LAYOUT:
|
||||
length = len(frame.dat)
|
||||
malformed |= length != NORMAL_BUS0_LAYOUT[key]
|
||||
received[key] = length
|
||||
return packets
|
||||
|
||||
if hyundai_probe(observe):
|
||||
return 'hyundai'
|
||||
# Rejecting HKG or Ford does not reject GM. Finish the shared passive window
|
||||
# within its original budget even when an early frame excludes Ford.
|
||||
while not (not malformed and received == NORMAL_BUS0_LAYOUT) and reads < 24 and clock() < deadline:
|
||||
if malformed and (gm_probe is None or gm_malformed):
|
||||
break
|
||||
observe(wait_for_one=True)
|
||||
if not malformed and received == NORMAL_BUS0_LAYOUT and normal_bus0_queries():
|
||||
return 'ford'
|
||||
if gm_probe is not None and not gm_malformed and gm_probe(gm_received):
|
||||
return 'gm'
|
||||
return None
|
||||
|
||||
@@ -21,11 +21,6 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
DRIVABLE_GEARS = (structs.CarState.GearShifter.low, structs.CarState.GearShifter.manumatic)
|
||||
|
||||
def update(self, can_packets):
|
||||
if self.CC is not None and self.CC.bp_lat is not None:
|
||||
self.CC.bp_lat.observe_can(can_packets, self.CC.CAN.main)
|
||||
return super().update(can_packets)
|
||||
|
||||
@staticmethod
|
||||
def get_pid_accel_limits(CP, current_speed, cruise_speed):
|
||||
# PCM doesn't allow acceleration near cruise_speed,
|
||||
|
||||
@@ -162,8 +162,6 @@ class FordLateralController:
|
||||
|
||||
def __init__(self, CP):
|
||||
self.CP = CP
|
||||
from opendbc.car.ford.pinion import Measurement
|
||||
self.measurement = Measurement(CP)
|
||||
self.model = None
|
||||
self.time_indices = ()
|
||||
self.lookahead = CURVATURE_LOOKAHEAD_MIN
|
||||
@@ -233,8 +231,9 @@ class FordLateralController:
|
||||
return MACH_E_HIGH_SPEED_LOOKAHEAD_EXTRA * float(np.interp(
|
||||
v_ego, [MACH_E_HIGH_SPEED_LOOKAHEAD_START_SPEED, MACH_E_HIGH_SPEED_LOOKAHEAD_FULL_SPEED], [0.0, 1.0]))
|
||||
|
||||
def _current_curvature(self, CS) -> float:
|
||||
return self.measurement.curvature(CS.out)
|
||||
@staticmethod
|
||||
def _current_curvature(CS) -> float:
|
||||
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
|
||||
def _driver_assisting_curve(self, CS, desired: float) -> bool:
|
||||
v_ego = float(CS.out.vEgoRaw)
|
||||
|
||||
@@ -114,14 +114,8 @@ def qualified(CP) -> bool:
|
||||
(FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0)))
|
||||
|
||||
|
||||
def path_speed_window_supported(CP) -> bool:
|
||||
return (qualified(CP) and CP.alternativeExperience == 32 and
|
||||
len(CP.safetyConfigs) == 1 and CP.safetyConfigs[0].safetyParam ==
|
||||
int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED | FordSafetyFlags.LONG_CONTROL))
|
||||
|
||||
|
||||
def bounded_command(owner: MachELateralController, demanded: FordLateralResult, previous: float, speed: float,
|
||||
previous_path_angle: float = 0.0, speed_window=None) -> FordLateralResult:
|
||||
previous_path_angle: float = 0.0) -> FordLateralResult:
|
||||
"""Intersect original demand with modern transient limits and old absolute cap.
|
||||
|
||||
A discontinuous speed change may leave no legal active interval. Withdraw the
|
||||
@@ -136,12 +130,9 @@ def bounded_command(owner: MachELateralController, demanded: FordLateralResult,
|
||||
else:
|
||||
curvature = float(np.clip(demanded.curvature, low, high))
|
||||
path_angle = 0.0
|
||||
# Native path assistance checks the maximum of six raw ABS speed samples.
|
||||
# Do not emit an assist that only the newest (lower) speed sample permits.
|
||||
path_speed = max(speed, speed_window[1]) if speed_window is not None else speed
|
||||
if (3.0 <= speed and path_speed < MACH_E_PATH_ANGLE_MAX_SPEED and abs(curvature) >= 0.0195 and
|
||||
if (3.0 <= speed < MACH_E_PATH_ANGLE_MAX_SPEED and abs(curvature) >= 0.0195 and
|
||||
curvature * demanded.path_angle > 0.0):
|
||||
headroom = max(0.0, (MAX_LATERAL_ACCEL / path_speed ** 2 - abs(curvature)) * path_speed)
|
||||
headroom = max(0.0, (MAX_LATERAL_ACCEL / speed ** 2 - abs(curvature)) * speed)
|
||||
path_cap = min(MACH_E_PATH_ANGLE_MAX, headroom)
|
||||
path_low = max(-path_cap, previous_path_angle - MACH_E_PATH_ANGLE_STEP)
|
||||
path_high = min(path_cap, previous_path_angle + MACH_E_PATH_ANGLE_STEP)
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Init-only pinion measurement selection adapted from BluePilot PR #145.
|
||||
|
||||
See CREDITS.md and THIRD_PARTY_NOTICES.md for upstream attribution and license.
|
||||
The geometry is carried in the existing exact Panda safety word, not live Params.
|
||||
"""
|
||||
from opendbc.car.ford.values import CAR
|
||||
|
||||
KEY = "FordPrefSteerAngleCurvature"
|
||||
ENABLE = 0x200
|
||||
SHIFT = 10
|
||||
MASK = 0x3E00
|
||||
GEOMETRY = {
|
||||
CAR.FORD_BRONCO_SPORT_MK1: 1, CAR.FORD_ESCAPE_MK4: 2, CAR.FORD_ESCAPE_MK4_5: 3,
|
||||
CAR.FORD_EXPEDITION_MK4: 4, CAR.FORD_EXPLORER_MK6: 5, CAR.FORD_FOCUS_MK4: 6,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1: 7, CAR.FORD_F_150_MK14: 8, CAR.FORD_MAVERICK_MK1: 9,
|
||||
CAR.FORD_MONDEO_MK5: 10, CAR.FORD_MUSTANG_MACH_E_MK1: 11, CAR.FORD_RANGER_MK2: 12,
|
||||
}
|
||||
CLASSIC = frozenset((32, 33, 128, 129, 160, 161))
|
||||
GENERIC = frozenset((66, 67, 130, 131, 194, 195))
|
||||
MACH_E = frozenset((18, 19, 130, 131, 146, 147))
|
||||
PROFILES = {i: CLASSIC if i in (1, 2, 5, 6, 9) else MACH_E if i == 11 else frozenset((10, 11)) if i == 10 else GENERIC
|
||||
for i in range(1, 13)}
|
||||
|
||||
|
||||
def decode(word):
|
||||
"""Return (original profile, geometry), or None for malformed metadata."""
|
||||
if not word & MASK:
|
||||
return word, 0
|
||||
index = (word >> SHIFT) & 15
|
||||
base = word & ~MASK
|
||||
if not word & ENABLE or index not in PROFILES or (base & ~256) not in PROFILES[index]:
|
||||
return None
|
||||
return base, index
|
||||
|
||||
|
||||
def base_word(word):
|
||||
result = decode(int(word))
|
||||
return result[0] if result is not None else -1
|
||||
|
||||
|
||||
def configured(cp):
|
||||
if cp.brand != "ford" or not cp.safetyConfigs:
|
||||
return False
|
||||
result = decode(int(cp.safetyConfigs[-1].safetyParam))
|
||||
return bool(result is not None and result[1] and result[1] == GEOMETRY.get(cp.carFingerprint))
|
||||
|
||||
|
||||
def select(cp, enabled):
|
||||
if not enabled or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar:
|
||||
return False
|
||||
from opendbc.car.ford.aol import qualified
|
||||
if not qualified(cp):
|
||||
return False
|
||||
index = GEOMETRY.get(cp.carFingerprint)
|
||||
if index is None:
|
||||
return False
|
||||
cp.safetyConfigs[-1].safetyParam |= ENABLE | index << SHIFT
|
||||
return True
|
||||
|
||||
|
||||
class Measurement:
|
||||
"""Same init-time family as native; live model correction never changes the source."""
|
||||
def __init__(self, cp):
|
||||
self.enabled = configured(cp)
|
||||
self.offset = self.roll = 0.0
|
||||
self.model = None
|
||||
if self.enabled:
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
self.model = VehicleModel(cp)
|
||||
|
||||
def update_parameters(self, parameters):
|
||||
import math
|
||||
if self.model is None or parameters is None:
|
||||
return
|
||||
values = (parameters.angleOffsetDeg, parameters.roll, parameters.stiffnessFactor, parameters.steerRatio)
|
||||
if parameters.valid and all(math.isfinite(v) for v in values):
|
||||
self.offset, self.roll = values[:2]
|
||||
self.model.update_params(max(values[2], 0.1), max(values[3], 0.1))
|
||||
|
||||
def curvature(self, state):
|
||||
if not self.enabled:
|
||||
return -state.yawRate / max(state.vEgoRaw, 0.1)
|
||||
import math
|
||||
return -self.model.calc_curvature(math.radians(state.steeringAngleDeg - self.offset), state.vEgoRaw, self.roll)
|
||||
@@ -3,7 +3,6 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralController
|
||||
from opendbc.car.ford.values import CAR
|
||||
|
||||
@@ -37,7 +36,7 @@ def test_typed_saved_blends_reach_owner_without_model(values, expected, monkeypa
|
||||
for key, value in zip(keys, values, strict=True):
|
||||
saved.put(key, value, block=True)
|
||||
inputs = controller_extensions.ManualTurnInputs(saved)
|
||||
owner = FordLateralController(CarInterface.get_non_essential_params(CAR.FORD_RANGER_MK2))
|
||||
owner = FordLateralController(SimpleNamespace(carFingerprint=CAR.FORD_RANGER_MK2))
|
||||
calls = []
|
||||
setter = owner.set_blend_settings
|
||||
|
||||
@@ -67,7 +66,7 @@ def test_typed_saved_blends_reach_owner_without_model(values, expected, monkeypa
|
||||
@pytest.mark.parametrize("desired,predicted,direction", ((-0.0005, -0.002, 1), (0.0005, 0.002, 2),
|
||||
(-0.001, -0.003, 2), (0.001, 0.003, 1)))
|
||||
def test_saved_blends_preserve_source_units_and_lane_direction(desired, predicted, direction):
|
||||
owner = FordLateralController(CarInterface.get_non_essential_params(CAR.FORD_RANGER_MK2))
|
||||
owner = FordLateralController(SimpleNamespace(carFingerprint=CAR.FORD_RANGER_MK2))
|
||||
owner.set_blend_settings(0.25, 0.65, 1.1)
|
||||
owner.model = SimpleNamespace(meta=SimpleNamespace(laneChangeState=2, laneChangeDirection=direction))
|
||||
blend = 0.25 + min(abs(desired) / 0.001, 1.0) * 0.4
|
||||
|
||||
@@ -80,10 +80,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
|
||||
controls.publish(command, lateral_log)
|
||||
_, packets = ci.apply(command.as_reader(), 1_000_000_000)
|
||||
assert packets
|
||||
assert not ci.CC.classic_extended_announced
|
||||
heartbeat = [data for address, data, _ in packets if address == 0x3CA]
|
||||
assert heartbeat and all(not (data[4] & 2) for data in heartbeat)
|
||||
assert all(((data[0] >> 4) & 7) == 0 for address, data, _ in packets if address == 0x3D6)
|
||||
assert ci.CC.classic_extended_announced
|
||||
assert not owner.human_turn_enabled
|
||||
assert (owner.curvature_blend_low, owner.curvature_blend_high, owner.curvature_lane_change_factor) == (0.25, 0.65, 1.1)
|
||||
finally:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Explorer fixed preview must not broaden the admitted Mach-E strategy."""
|
||||
from types import SimpleNamespace
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -8,11 +7,6 @@ from opendbc.car.ford.explorer_lateral import ExplorerLateralController
|
||||
from opendbc.car.ford.mache_lateral import MachELateralController
|
||||
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags
|
||||
|
||||
def controller_params(car, flags):
|
||||
cp = CarInterface.get_non_essential_params(car)
|
||||
return SimpleNamespace(brand=cp.brand, safetyConfigs=cp.safetyConfigs, carFingerprint=car, flags=flags)
|
||||
|
||||
|
||||
|
||||
def model():
|
||||
return SimpleNamespace(orientationRate=SimpleNamespace(z=[i * 0.01 for i in range(33)]))
|
||||
@@ -20,14 +14,14 @@ def model():
|
||||
|
||||
@pytest.mark.parametrize("delay", (0.20, 0.30, 0.40))
|
||||
def test_explorer_fixed_lookahead_selects_original_model_sample(delay):
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
owner.set_inputs(model(), tuple(i * 0.1 for i in range(33)), delay, True)
|
||||
assert owner._curvature_lookahead() == 0.20
|
||||
assert owner._predicted_curvature(10.0, owner._curvature_lookahead()) == pytest.approx(0.002)
|
||||
|
||||
|
||||
def test_mach_e_compatibility_keeps_current_provider_lookahead():
|
||||
owner = MachELateralController(controller_params(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD))
|
||||
owner = MachELateralController(SimpleNamespace(carFingerprint=CAR.FORD_MUSTANG_MACH_E_MK1, flags=FordFlags.CANFD))
|
||||
owner.set_inputs(model(), tuple(i * 0.1 for i in range(33)), 0.40, True)
|
||||
assert owner._curvature_lookahead() == 0.40
|
||||
assert owner._predicted_curvature(10.0, owner._curvature_lookahead()) == pytest.approx(0.004)
|
||||
@@ -37,7 +31,7 @@ def test_explorer_strategy_rejects_unrelated_topology():
|
||||
for car, flags in ((CAR.FORD_EXPLORER_MK6, FordFlags.CANFD),
|
||||
(CAR.FORD_EDGE_MK2, FordFlags.ALT_STEER_ANGLE | FordFlags.NEW_PORT)):
|
||||
with pytest.raises(ValueError):
|
||||
ExplorerLateralController(controller_params(car, flags))
|
||||
ExplorerLateralController(SimpleNamespace(carFingerprint=car, flags=flags))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alpha,release", ((False, False), (True, False), (False, True), (True, True)))
|
||||
@@ -65,7 +59,7 @@ def test_actual_interface_restores_explicit_longitudinal_ownership(alpha, releas
|
||||
def test_classic_modern_envelope_withdraws_infeasible_speed_transition():
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralResult
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
result = bounded_command(owner, FordLateralResult(curvature=.02, active=True), .02, 20., .02)
|
||||
assert not result.active
|
||||
assert result.curvature == owner.curvature_last == 0.
|
||||
@@ -75,7 +69,7 @@ def test_classic_modern_envelope_withdraws_infeasible_speed_transition():
|
||||
def test_manual_turn_withdraws_without_mach_e_latch_or_gain_compensation():
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralResult
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
owner.manual_turn_detected = True
|
||||
result = bounded_command(owner, FordLateralResult(active=True), .002, 10., .002)
|
||||
assert not result.active
|
||||
@@ -86,7 +80,7 @@ def test_manual_turn_withdraws_without_mach_e_latch_or_gain_compensation():
|
||||
def test_speed_change_cap_cannot_escape_original_rate_interval():
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralResult
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
# At26m/s modernjerk admits~.000265, originalnative lookup only.000180.
|
||||
# Modern bank-tolerant cap~.005309 requires.000211 fromprevious.005520.
|
||||
result = bounded_command(owner, FordLateralResult(curvature=.00552, active=True), .00552, 26., .00552)
|
||||
@@ -137,7 +131,7 @@ def test_typed_offset_eps_and_namespace_guards():
|
||||
def test_final_intersection_preserves_source_rise_and_unwind(sign):
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralResult
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
rise = bounded_command(owner, FordLateralResult(curvature=sign*.0015, active=True), sign*.001, 26., sign*.001)
|
||||
assert rise.curvature == pytest.approx(sign*.00108)
|
||||
fall = bounded_command(owner, FordLateralResult(curvature=0., active=True), sign*.001, 26., sign*.001)
|
||||
@@ -164,7 +158,7 @@ def test_classic_rate_endpoint_has_correct_decoded_sign(rate, expected):
|
||||
def test_measured_recovery_with_empty_source_interval_withdraws(sign):
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralResult
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
command = bounded_command(owner, FordLateralResult(curvature=sign*.008, active=True), 0., 35., sign*.008)
|
||||
assert not command.active and command.curvature == 0.
|
||||
assert owner.curvature_last == owner.path_angle_last == 0.
|
||||
@@ -174,7 +168,7 @@ def test_measured_recovery_with_empty_source_interval_withdraws(sign):
|
||||
def test_measured_window_requires_recovery_and_bounds_unwind(sign):
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralResult
|
||||
owner = ExplorerLateralController(controller_params(CAR.FORD_EXPLORER_MK6, 0))
|
||||
owner = ExplorerLateralController(SimpleNamespace(carFingerprint=CAR.FORD_EXPLORER_MK6, flags=0))
|
||||
# Recovery outside the error window may proceed when source fall permits it.
|
||||
recovery = bounded_command(owner, FordLateralResult(curvature=sign*.001, active=True), sign*.003, 35., 0.)
|
||||
delta = CarControllerParams.CURVATURE_LIMITS.MAX_LATERAL_JERK / 35.**2 * .05
|
||||
|
||||
@@ -27,8 +27,7 @@ class RecordedPreview:
|
||||
pass
|
||||
|
||||
def apply_blend_settings(self, controller):
|
||||
lateral = controller.mache_lateral or controller.classic_lateral
|
||||
lateral.set_blend_settings(.4, .4, .85)
|
||||
controller.mache_lateral.set_blend_settings(.4, .4, .85)
|
||||
|
||||
def lateral_snapshot(self, speed):
|
||||
return self.model, ModelConstants.T_IDXS, .2, True
|
||||
@@ -37,10 +36,10 @@ class RecordedPreview:
|
||||
return (), False
|
||||
|
||||
|
||||
def run_turn(*, hands_free, angle, torque, desired, speed, yaw, identity=CAR.FORD_MUSTANG_MACH_E_MK1):
|
||||
def run_turn(*, hands_free, angle, torque, desired, speed, yaw):
|
||||
native = test_ford_aol.TestFordAolDriverIntent()
|
||||
native.setUp()
|
||||
native.reset(18 if identity == CAR.FORD_MUSTANG_MACH_E_MK1 else 66)
|
||||
native.reset(18)
|
||||
native.arm()
|
||||
with OpenpilotPrefix():
|
||||
saved = Params()
|
||||
@@ -48,9 +47,9 @@ def run_turn(*, hands_free, angle, torque, desired, speed, yaw, identity=CAR.FOR
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[0].update({0x5A: 8, 0x3A6: 8, 0x3A7: 8})
|
||||
fingerprint[2].update({0x3D6: 8, 0x186: 8})
|
||||
cp = CarInterface.get_params(identity, fingerprint, [], False, False, False)
|
||||
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint, [], False, False, False)
|
||||
cp.alternativeExperience = 32
|
||||
assert cp.safetyConfigs[-1].safetyParam == (18 if identity == CAR.FORD_MUSTANG_MACH_E_MK1 else 66)
|
||||
assert cp.safetyConfigs[-1].safetyParam == 18
|
||||
assert cp.flags == 3
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
controller.hands_free_cluster_input = FordClusterPreference(saved)
|
||||
@@ -72,18 +71,6 @@ def run_turn(*, hands_free, angle, torque, desired, speed, yaw, identity=CAR.FOR
|
||||
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)
|
||||
if identity != CAR.FORD_MUSTANG_MACH_E_MK1:
|
||||
lateral_frames = [(index, data) for index, (address, data, _) in enumerate(frames) if address == 0x3D6]
|
||||
announcements = [(index, data) for index, (address, data, _) in enumerate(frames) if address == 0x3CA]
|
||||
assert len(announcements) <= 1
|
||||
for index, data in lateral_frames:
|
||||
active = ((data[0] >> 4) & 7) != 0
|
||||
previous_active = bool(steering and ((steering[-1][0] >> 4) & 7))
|
||||
if active != previous_active:
|
||||
assert announcements and announcements[0][0] < index
|
||||
assert bool(announcements[0][1][4] & 2) == active
|
||||
if not command.latActive:
|
||||
assert all(not (data[4] & 2) for _, data in announcements)
|
||||
for address, data, bus in frames:
|
||||
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
if address == 0x3D6:
|
||||
@@ -110,10 +97,6 @@ def run_turn(*, hands_free, angle, torque, desired, speed, yaw, identity=CAR.FOR
|
||||
cs.steeringAngleDeg = angle
|
||||
cs.steeringTorque = torque
|
||||
cs.rightBlinker = True
|
||||
if identity != CAR.FORD_MUSTANG_MACH_E_MK1:
|
||||
# Generic curvature retains its existing 45deg/3s preturned detector.
|
||||
for _ in range(310):
|
||||
step()
|
||||
count = len(displays)
|
||||
for _ in range(40):
|
||||
frames = step()
|
||||
@@ -172,13 +155,3 @@ def test_cluster_preference_survives_native_cleanup(tmp_path, saved, flag):
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity', [CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2])
|
||||
def test_generic_curvature_native_display_handoff_retains_hands_free_during_manual_turn(identity):
|
||||
# Nonzero demand exercises the bounded first-active command, not just UI.
|
||||
inputs = dict(identity=identity, angle=-55., torque=-2., desired=.000041086183, speed=2.55, yaw=-.006)
|
||||
hands_free = run_turn(hands_free=True, **inputs)
|
||||
plain = run_turn(hands_free=False, **inputs)
|
||||
assert plain == hands_free
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Original-derived Mach-E demand and driver handoff regressions."""
|
||||
from types import SimpleNamespace
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
import pytest
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.lateral import MAX_LATERAL_JERK
|
||||
@@ -12,15 +11,10 @@ from opendbc.car.ford.tests.test_three_ports import params
|
||||
from opendbc.car.ford.carcontroller import CarController
|
||||
from opendbc.car.ford.carstate import CarState
|
||||
|
||||
def controller_params(car, flags):
|
||||
cp = CarInterface.get_non_essential_params(car)
|
||||
return SimpleNamespace(brand=cp.brand, safetyConfigs=cp.safetyConfigs, carFingerprint=car, flags=flags)
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller():
|
||||
owner = MachELateralController(controller_params(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD))
|
||||
owner = MachELateralController(SimpleNamespace(flags=FordFlags.CANFD, carFingerprint=CAR.FORD_MUSTANG_MACH_E_MK1))
|
||||
model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.]*33),
|
||||
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
|
||||
owner.set_inputs(model, tuple(i*.1 for i in range(33)), .2, True)
|
||||
@@ -379,7 +373,7 @@ def test_actual_controller_announces_before_full_physical_frame_and_resets_off()
|
||||
|
||||
|
||||
def test_modern_transient_bound_updates_emitted_history_without_hidden_windup():
|
||||
owner = MachELateralController(controller_params(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD))
|
||||
owner = MachELateralController(SimpleNamespace(flags=FordFlags.CANFD, carFingerprint=CAR.FORD_MUSTANG_MACH_E_MK1))
|
||||
raw = FordLateralResult(curvature=.006, path_angle=.16, active=True)
|
||||
command = bounded_command(owner, raw, .001, 15.)
|
||||
assert command.curvature < raw.curvature
|
||||
@@ -395,7 +389,7 @@ def test_modern_transient_bound_updates_emitted_history_without_hidden_windup():
|
||||
|
||||
@pytest.mark.parametrize("sign", (-1, 1))
|
||||
def test_speed_changes_intersect_bounds_or_withdraw_and_preserve_path_headroom(sign):
|
||||
owner = MachELateralController(controller_params(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD))
|
||||
owner = MachELateralController(SimpleNamespace(flags=FordFlags.CANFD, carFingerprint=CAR.FORD_MUSTANG_MACH_E_MK1))
|
||||
previous, path = sign * .0198, sign * .055
|
||||
for speed in (5., 5.1, 5.2, 5.3, 8., 15., 5.):
|
||||
demand = FordLateralResult(curvature=sign*.02, path_angle=sign*.16, active=True)
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import pytest
|
||||
from opendbc.car.ford import mache_lateral as original
|
||||
from opendbc.car import Bus
|
||||
from opendbc.can import CANPacker
|
||||
|
||||
candidate = original
|
||||
|
||||
|
||||
def cp():
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
from opendbc.car.ford.values import CAR
|
||||
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[0] = {0x5A: 8, 0x3A6: 8, 0x3A7: 8}
|
||||
result = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint, [], True, False, False)
|
||||
result.alternativeExperience = 32
|
||||
assert result.safetyConfigs[-1].safetyParam == 19 and result.flags == 3
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sign", [-1, 1])
|
||||
def test_recorded_native_window_prevents_first_rejection_and_cascade(sign):
|
||||
from opendbc.safety.tests.test_ford_mach_e_extended import TestFordMachEExtended
|
||||
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent
|
||||
|
||||
f = TestFordMachEExtended()
|
||||
f.SAFETY_PARAM = 19
|
||||
f.setUp()
|
||||
a = TestFordAolDriverIntent()
|
||||
a.setUp()
|
||||
|
||||
def setup():
|
||||
a.reset(19)
|
||||
a.arm()
|
||||
a.pump(12, mask=3, speed=8.805556, curvature=sign * 0.02)
|
||||
f._rx(f._pcm_status_msg(True))
|
||||
a.safety.aol_set_host_request(3)
|
||||
assert a.safety.aol_get_permission_mask() == 3
|
||||
assert f.announce()
|
||||
a.pump(2, mask=3, main=4, speed=8.775, curvature=sign * 0.02)
|
||||
f._set_prev_desired_angle(sign * 0.02)
|
||||
|
||||
demand = original.FordLateralResult(active=True, curvature=sign * 0.02, path_angle=sign * 0.0035, curvature_rate=-0.001023)
|
||||
setup()
|
||||
old = original.bounded_command(candidate.MachELateralController(cp()), demand, sign * 0.02, 8.775)
|
||||
assert not f._tx(f.command(curvature=old.curvature, angle=old.path_angle, rate=old.curvature_rate, increment_timer=False))
|
||||
assert not f._tx(f.command(curvature=sign * 0.02, angle=0, rate=0, increment_timer=False))
|
||||
setup()
|
||||
new = candidate.bounded_command(candidate.MachELateralController(cp()), demand, sign * 0.02, 8.775, speed_window=(8.775, 8.806, 0.031))
|
||||
assert new.curvature == old.curvature and new.path_angle == 0
|
||||
assert f._tx(f.command(curvature=new.curvature, angle=new.path_angle, rate=new.curvature_rate, increment_timer=False))
|
||||
assert f._tx(f.command(curvature=sign * 0.02, angle=0, rate=0, increment_timer=False))
|
||||
a.tearDown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("speed", [0, 2.99, 3, 5, 8.7, 8.8, 12, 30])
|
||||
@pytest.mark.parametrize("active", [False, True])
|
||||
def test_zero_span_window_preserves_default_command(speed, active):
|
||||
d = original.FordLateralResult(active=active, curvature=0.02, path_angle=0.1)
|
||||
assert candidate.bounded_command(candidate.MachELateralController(cp()), d, 0.019, speed) == candidate.bounded_command(
|
||||
candidate.MachELateralController(cp()), d, 0.019, speed, speed_window=(speed, speed, 0.0)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param,ae,supported", [(19, 32, True), (18, 32, False), (19, 0, False), (147, 32, False), (67, 32, False)])
|
||||
def test_exact_profile(param, ae, supported):
|
||||
c = cp()
|
||||
c.safetyConfigs[0].safetyParam = param
|
||||
c.alternativeExperience = ae
|
||||
assert candidate.path_speed_window_supported(c) == supported
|
||||
|
||||
|
||||
def test_actual_carstate_tracks_six_raw_samples():
|
||||
from opendbc.car.ford.carstate import CarState
|
||||
|
||||
c = cp()
|
||||
cs = CarState(c)
|
||||
parsers = cs.get_can_parsers(c)
|
||||
p = CANPacker("ford_lincoln_base_pt")
|
||||
values = [8.7, 8.805556, 8.79, 8.78, 8.775, 8.77, 8.76]
|
||||
for i, v in enumerate(values):
|
||||
parsers[Bus.pt].update(
|
||||
[[(i + 1) * 20_000_000, [p.make_can_msg("BrakeSysFeatures", 0, {"Veh_V_ActlBrk": v * 3.6, "VehVActlBrk_D_Qf": 3, "VehVActlBrk_No_Cnt": i + 1})]]]
|
||||
)
|
||||
cs.update(parsers)
|
||||
assert cs.bluepilot_speed_window is not None
|
||||
assert cs.bluepilot_speed_samples is not None
|
||||
assert cs.bluepilot_speed_window[1] >= 8.8
|
||||
assert len(cs.bluepilot_speed_samples) == 6
|
||||
|
||||
for i in range(7, 13):
|
||||
parsers[Bus.pt].update(
|
||||
[[(i + 1) * 20_000_000, [p.make_can_msg("BrakeSysFeatures", 0, {"Veh_V_ActlBrk": 8.7 * 3.6, "VehVActlBrk_D_Qf": 3, "VehVActlBrk_No_Cnt": i + 1})]]]
|
||||
)
|
||||
cs.update(parsers)
|
||||
assert cs.bluepilot_speed_window is not None
|
||||
assert cs.bluepilot_speed_window[1] < 8.8
|
||||
|
||||
|
||||
@pytest.mark.parametrize("high", [8.79, 8.8, 8.81, 9.0])
|
||||
def test_rising_window_previous_nonzero_path(high):
|
||||
demand = original.FordLateralResult(active=True, curvature=0.02, path_angle=0.05)
|
||||
out = candidate.bounded_command(candidate.MachELateralController(cp()), demand, 0.02, 8.775, 0.05, speed_window=(8.7, high, 0.1))
|
||||
if high >= 8.8:
|
||||
assert out.path_angle == 0
|
||||
else:
|
||||
assert 0 <= out.path_angle <= 0.05
|
||||
assert out.curvature == 0.02
|
||||
|
||||
|
||||
def test_controller_uses_native_speed_window():
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.carcontroller import CarController
|
||||
from opendbc.car.ford.carstate import CarState
|
||||
from opendbc.car.ford.values import DBC
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
with OpenpilotPrefix():
|
||||
c = cp()
|
||||
controller = CarController(DBC[c.carFingerprint], c)
|
||||
controller.manual_turn_inputs = None
|
||||
controller.mache_extended_announced = True
|
||||
assert controller.mache_lateral is not None
|
||||
controller.mache_lateral.curvature_last = 0.02
|
||||
controller.mache_lateral.update = lambda *_: candidate.FordLateralResult(active=True, curvature=0.02, path_angle=0.0035)
|
||||
state = CarState(c)
|
||||
state.update(state.get_can_parsers(c))
|
||||
state.bluepilot_speed_window = (8.775, 8.806, 0.031)
|
||||
cs = structs.CarState(canValid=True, vEgo=8.775, vEgoRaw=8.775, gearShifter=structs.CarState.GearShifter.drive)
|
||||
cs.cruiseState.available = True
|
||||
state.out = cs.as_reader()
|
||||
command = structs.CarControl(latActive=True, longActive=True)
|
||||
_, frames = controller.update(command.as_reader(), state, 1_000_000_000)
|
||||
parser = CANParser("ford_lincoln_base_pt", [("LateralMotionControl2", 50)], 0)
|
||||
parser.update([[1_000_000_000, frames]])
|
||||
assert any(address == 0x3D6 for address, _, _ in frames)
|
||||
assert abs(parser.vl["LateralMotionControl2"]["LatCtlPath_An_Actl"]) < 0.00001
|
||||
assert abs(abs(parser.vl["LateralMotionControl2"]["LatCtlCurv_No_Actl"]) - 0.02) < 0.00001
|
||||
@@ -50,11 +50,11 @@ def test_mache_idle_leaves_stock_display_and_fcw_untouched_then_owns_active_ui()
|
||||
assert state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('identity,aol,expected', [(CAR.FORD_MUSTANG_MACH_E_MK1, False, UI), (CAR.FORD_F_150_MK14, True, set())])
|
||||
def test_idle_ui_respects_native_display_ownership(identity, aol, expected):
|
||||
@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 == expected
|
||||
assert seen == UI
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
"""Closed, init-only measurement selection; disabled behavior stays unchanged."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
from opendbc.car.ford.pinion import GEOMETRY, ENABLE, SHIFT, MASK, decode, select, configured, Measurement
|
||||
from opendbc.car.ford.aol import qualified
|
||||
from opendbc.car.vehicle_model import VehicleModel, calc_slip_factor
|
||||
from opendbc.can.packer import CANPacker
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def params(car, long=False):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[0][0x5A] = 8
|
||||
fp[2].update({0x3D6: 8, 0x186: 8})
|
||||
return CarInterface.get_params(car, fp, [], long, False, False)
|
||||
|
||||
|
||||
def test_closed_decoder_matches_native_exhaustively():
|
||||
n = libsafety_py.libsafety
|
||||
for word in range(65536):
|
||||
decoded = decode(word)
|
||||
assert n.get_ford_pinion_base(word) == (decoded[0] if decoded is not None else 65535)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car', list(GEOMETRY))
|
||||
@pytest.mark.parametrize('long', [False, True])
|
||||
def test_default_off_and_exact_geometry_selection(car, long):
|
||||
cp = params(car, long)
|
||||
before = cp.to_dict()
|
||||
assert not select(cp, False)
|
||||
assert cp.to_dict() == before
|
||||
assert select(cp, True)
|
||||
assert configured(cp) and qualified(cp)
|
||||
assert decode(cp.safetyConfigs[-1].safetyParam)[1] == GEOMETRY[car]
|
||||
# Cross-family/identity corruption is not accepted by the host.
|
||||
cp.safetyConfigs[-1].safetyParam = (cp.safetyConfigs[-1].safetyParam & ~MASK) | ENABLE | ((GEOMETRY[car] % 12 + 1) << SHIFT)
|
||||
assert not configured(cp) and not qualified(cp)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car', list(GEOMETRY))
|
||||
def test_geometry_matches_actual_vehicle_model_and_native_sign(car):
|
||||
cp = params(car)
|
||||
index = GEOMETRY[car]
|
||||
n = libsafety_py.libsafety
|
||||
vm = VehicleModel(cp)
|
||||
assert n.get_ford_pinion_slip(index) == pytest.approx(calc_slip_factor(vm), abs=2e-10)
|
||||
assert n.get_ford_pinion_ratio(index) == pytest.approx(cp.steerRatio)
|
||||
assert n.get_ford_pinion_wheelbase(index) == pytest.approx(cp.wheelbase)
|
||||
assert select(cp, True)
|
||||
host = Measurement(cp)
|
||||
packer = CANPacker('ford_lincoln_base_pt')
|
||||
for speed in (0., 5., 15., 30.):
|
||||
for angle in (-100., -10., 0., 10., 100.):
|
||||
n.set_alternative_experience(0)
|
||||
n.set_safety_hooks(structs.CarParams.SafetyModel.ford, cp.safetyConfigs[-1].safetyParam)
|
||||
assert n.get_ford_pinion_enabled()
|
||||
for counter in range(6):
|
||||
a, d, b = packer.make_can_msg('BrakeSysFeatures', 0, {'Veh_V_ActlBrk': speed * 3.6, 'VehVActlBrk_D_Qf': 3})
|
||||
assert n.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d))
|
||||
a, d, b = packer.make_can_msg('SteeringPinion_Data', 0, {'StePinComp_An_Est': angle, 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': counter})
|
||||
assert n.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d))
|
||||
state = SimpleNamespace(vEgoRaw=max(speed, .1), yawRate=99., steeringAngleDeg=angle)
|
||||
expected = -host.curvature(state) * 50000
|
||||
assert n.get_curvature_meas_min() == pytest.approx(expected, abs=1.)
|
||||
assert n.get_curvature_meas_max() == pytest.approx(expected, abs=1.)
|
||||
# A valid but contradictory yaw packet never changes selected pinion measurement.
|
||||
a, d, b = packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': 1., 'VehYawWActl_D_Qf': 3})
|
||||
assert n.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d))
|
||||
assert n.get_curvature_meas_min() == pytest.approx(expected, abs=1.)
|
||||
|
||||
|
||||
def test_default_measurement_is_yaw_even_with_conflicting_pinion():
|
||||
cp = params(next(iter(GEOMETRY)))
|
||||
m = Measurement(cp)
|
||||
state = SimpleNamespace(vEgoRaw=15., yawRate=.3, steeringAngleDeg=100.)
|
||||
assert m.curvature(state) == -.3 / 15.
|
||||
assert not m.enabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize('index,base', [(1, 32), (3, 66), (10, 10), (11, 18)])
|
||||
@pytest.mark.parametrize('long', [False, True])
|
||||
@pytest.mark.parametrize('manual', [False, True])
|
||||
def test_pinion_inventory_and_invalid_quality_withdraw_authority(index, base, long, manual):
|
||||
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent
|
||||
n = TestFordAolDriverIntent()
|
||||
n.setUp()
|
||||
base |= int(long) | (256 if manual else 0)
|
||||
n.reset(base)
|
||||
count = n.safety.get_ford_pinion_rx_count()
|
||||
n.safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, base | ENABLE | index << SHIFT)
|
||||
n.safety.set_aol_test_heartbeat(True)
|
||||
assert n.safety.get_ford_pinion_rx_count() == count + 1
|
||||
original_rx = n.rx
|
||||
|
||||
def rx(name, values):
|
||||
if name == 'Yaw_Data_FD1':
|
||||
original_rx('SteeringPinion_Data', {'StePinComp_An_Est': 0., 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': n.tick % 16})
|
||||
original_rx(name, values)
|
||||
n.rx = rx
|
||||
try:
|
||||
n.arm()
|
||||
assert n.safety.aol_get_permission_mask() == 1
|
||||
a, d, b = n.packer.make_can_msg('SteeringPinion_Data', 0,
|
||||
{'StePinComp_An_Est': 100., 'StePinCompAnEst_D_Qf': 0, 'StePinAn_No_Cnt': (n.tick + 1) % 16})
|
||||
assert not n.safety.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d))
|
||||
assert n.safety.aol_get_permission_mask() == 0
|
||||
assert n.safety.get_curvature_meas_max() == 0
|
||||
finally:
|
||||
n.tearDown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure', ['missing', 'wrong_bus', 'short', 'counter'])
|
||||
def test_pinion_integrity_is_required_not_substituted_by_healthy_yaw(failure):
|
||||
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent
|
||||
n = TestFordAolDriverIntent()
|
||||
n.setUp()
|
||||
n.reset(66)
|
||||
n.safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, 66 | ENABLE | 7 << SHIFT)
|
||||
n.safety.set_aol_test_heartbeat(True)
|
||||
original_rx = n.rx
|
||||
inject = True
|
||||
|
||||
def rx(name, values):
|
||||
if name == 'Yaw_Data_FD1':
|
||||
if inject:
|
||||
original_rx('SteeringPinion_Data', {'StePinComp_An_Est': 0., 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': n.tick % 16})
|
||||
elif failure != 'missing':
|
||||
a, d, b = n.packer.make_can_msg('SteeringPinion_Data', 0,
|
||||
{'StePinComp_An_Est': 100., 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': 2})
|
||||
n.safety.safety_rx_hook(libsafety_py.make_CANPacket(a, 1 if failure == 'wrong_bus' else b, d[:7] if failure == 'short' else d))
|
||||
original_rx(name, values)
|
||||
n.rx = rx
|
||||
try:
|
||||
n.arm()
|
||||
inject = False
|
||||
n.pump(120, mask=1)
|
||||
assert n.safety.aol_get_permission_mask() == 0
|
||||
finally:
|
||||
n.tearDown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car', [c for c in GEOMETRY if 'MONDEO' not in c])
|
||||
@pytest.mark.parametrize('long', [False, True])
|
||||
def test_angle_selection_preserves_pinion_and_aol_exact_profile(car, long):
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import select as angle_select, qualified as angle_qualified
|
||||
from opendbc.car.ford.aol import base_safety_param, AOL_WORDS
|
||||
cp = params(car, long)
|
||||
assert select(cp, True)
|
||||
assert angle_select(cp, 1)
|
||||
assert configured(cp) and angle_qualified(cp)
|
||||
cp.safetyConfigs[-1].safetyParam |= base_safety_param(cp)
|
||||
cp.alternativeExperience = 32
|
||||
assert cp.safetyConfigs[-1].safetyParam in AOL_WORDS
|
||||
assert configured(cp) and angle_qualified(cp) and qualified(cp, marked_only=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('word', [32 | ENABLE, 32 | (1 << SHIFT), 32 | ENABLE | (13 << SHIFT),
|
||||
66 | ENABLE | (1 << SHIFT), 32 | ENABLE | (1 << SHIFT) | 0x4000,
|
||||
32 | ENABLE | (1 << SHIFT) | 0x8000])
|
||||
def test_invalid_pinion_native_profile_denies_all_commands(word):
|
||||
n = libsafety_py.libsafety
|
||||
n.set_alternative_experience(32)
|
||||
n.set_safety_hooks(structs.CarParams.SafetyModel.ford, word)
|
||||
n.set_controls_allowed(True)
|
||||
assert not n.get_ford_pinion_enabled()
|
||||
for address in (0x3D3, 0x3D6, 0x3CA, 0x186, 0x18A, 0x3D8):
|
||||
assert not n.safety_tx_hook(libsafety_py.make_CANPacket(address, 0, bytes(8)))
|
||||
|
||||
|
||||
def test_pinion_rx_capacity_guard_withdraws_authority_without_overwrite():
|
||||
n = libsafety_py.libsafety
|
||||
n.set_alternative_experience(32)
|
||||
n.set_safety_hooks(structs.CarParams.SafetyModel.ford, 66 | ENABLE | (7 << SHIFT))
|
||||
assert n.test_ford_pinion_rx_overflow()
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Clock an equipped Bolt's replacement brake frame from the factory ACC cycle."""
|
||||
|
||||
class BoltAccBrakeClock:
|
||||
MIN_INTERVAL_NS = 30_000_000
|
||||
MAX_AGE_NS = 80_000_000
|
||||
|
||||
def __init__(self):
|
||||
self.command = None
|
||||
self.command_ns = 0
|
||||
self.source_ns = 0
|
||||
self.source_counter = None
|
||||
self.counter = -1
|
||||
self.emit_ns = 0
|
||||
self.active_last = False
|
||||
|
||||
def prepare(self, now_ns, brake, enabled, near_stop, full_stop):
|
||||
self.command = (brake, enabled, near_stop, full_stop)
|
||||
self.command_ns = now_ns
|
||||
|
||||
def release(self, now_ns):
|
||||
if not self.active_last or now_ns - self.emit_ns < self.MIN_INTERVAL_NS:
|
||||
return None
|
||||
# A lost clock cannot authorize braking. Retire the last active command
|
||||
# once with the canonical zero release; the native ownership gates still run.
|
||||
self.counter = (self.counter + 1) % 4
|
||||
self.emit_ns = now_ns
|
||||
self.active_last = False
|
||||
return (0, self.counter, False, False, False)
|
||||
|
||||
def sample(self, now_ns, source_ns, counter, *, active, main_ready, source_valid):
|
||||
if not main_ready:
|
||||
self.command = None
|
||||
self.active_last = False
|
||||
return None
|
||||
if (not source_valid or self.command is None or
|
||||
not 0 < self.command_ns <= now_ns or now_ns - self.command_ns > self.MAX_AGE_NS or
|
||||
not 0 < source_ns <= now_ns or now_ns - source_ns > self.MAX_AGE_NS or not 0 <= counter < 4):
|
||||
return self.release(now_ns)
|
||||
if source_ns <= self.source_ns or counter == self.source_counter:
|
||||
return self.release(now_ns) if now_ns - self.emit_ns > self.MAX_AGE_NS else None
|
||||
if self.emit_ns and now_ns - self.emit_ns < self.MIN_INTERVAL_NS:
|
||||
return None
|
||||
self.source_ns, self.source_counter = source_ns, counter
|
||||
if counter == self.counter:
|
||||
return None # The safe release may already have consumed this counter.
|
||||
self.counter, self.emit_ns = counter, now_ns
|
||||
brake, enabled, near_stop, full_stop = self.command
|
||||
self.active_last = bool(active and (brake > 0 or enabled))
|
||||
return (brake, counter, enabled, near_stop, full_stop) if active else (0, counter, False, False, False)
|
||||
@@ -7,7 +7,6 @@ from opendbc.car.gm.values import is_gm_auto_hold
|
||||
from opendbc.car.gm.ordinary import demands as ascm_demands
|
||||
from opendbc.car.gm.truck_longitudinal import TruckTuning, truck_tuning_supported
|
||||
import math
|
||||
from opendbc.car.gm.bolt_acc_brake_clock import BoltAccBrakeClock
|
||||
import numpy as np
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, structs
|
||||
@@ -267,7 +266,6 @@ class CarController(CarControllerBase):
|
||||
self.long_pitch_input = None
|
||||
self.bolt_removed_cancel_credit_used = 0
|
||||
self.bolt_pedal_removed = is_bolt_pedal_removed_profile(CP) or is_bolt_pedal_removed_profile(CP, stock_only=True)
|
||||
self.bolt_steering_ack_counter = is_bolt_pedal_profile(CP) or is_bolt_pedal_profile(CP, stock_only=True)
|
||||
self.bolt_present_no_acc_stock = is_bolt_present_no_acc_pedal_profile(CP, stock_only=True)
|
||||
self.truck_tuning = TruckTuning() if truck_tuning_supported(CP) else None
|
||||
self.truck_tuning_input = None
|
||||
@@ -302,8 +300,6 @@ class CarController(CarControllerBase):
|
||||
self.bolt_acc_pedal_friction_release_frames = 0
|
||||
self.bolt_acc_pedal_friction_low_speed_active = False
|
||||
self.bolt_acc_pedal_stop_hold = False
|
||||
self.bolt_acc_brake_clock = (BoltAccBrakeClock() if int(CP.safetyConfigs[0].safetyParam) == 0x1CD and
|
||||
CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and is_bolt_pedal_profile(CP) else None)
|
||||
|
||||
self.lka_steering_cmd_counter = 0
|
||||
self.lka_icon_status_last = (False, False)
|
||||
@@ -605,11 +601,7 @@ class CarController(CarControllerBase):
|
||||
if CS.loopback_lka_steering_cmd_ts_nanos == 0 or out_of_sync:
|
||||
steer_step = self.params.STEER_STEP
|
||||
|
||||
if self.bolt_steering_ack_counter:
|
||||
if CS.loopback_lka_steering_cmd_updated:
|
||||
self.lka_steering_cmd_counter = (CS.loopback_lka_steering_cmd_counter + 1) % 4
|
||||
else:
|
||||
self.lka_steering_cmd_counter += 1 if CS.loopback_lka_steering_cmd_updated else 0
|
||||
self.lka_steering_cmd_counter += 1 if CS.loopback_lka_steering_cmd_updated else 0
|
||||
|
||||
# Avoid GM EPS faults when transmitting messages too close together: skip this transmit if we
|
||||
# received the ASCMLKASteeringCmd loopback confirmation too recently
|
||||
@@ -862,16 +854,10 @@ class CarController(CarControllerBase):
|
||||
elif self.bolt_acc_pedal_friction_release_frames > 0:
|
||||
self.bolt_acc_pedal_friction_release_frames -= 1
|
||||
if friction_main_on or self.bolt_acc_pedal_friction_release_frames > 0:
|
||||
if self.bolt_acc_brake_clock is not None:
|
||||
self.bolt_acc_brake_clock.prepare(
|
||||
now_nanos, self.apply_brake, active and friction_main_on,
|
||||
active and CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE,
|
||||
active and CS.out.standstill and actuators.longControlState == LongCtrlState.stopping)
|
||||
else:
|
||||
can_sends.append(gmcan.create_friction_brake_command(
|
||||
self.packer_ch, CanBus.POWERTRAIN, self.apply_brake, (self.frame // 4) % 4,
|
||||
active and friction_main_on, active and CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE,
|
||||
active and CS.out.standstill and actuators.longControlState == LongCtrlState.stopping, self.CP))
|
||||
can_sends.append(gmcan.create_friction_brake_command(
|
||||
self.packer_ch, CanBus.POWERTRAIN, self.apply_brake, (self.frame // 4) % 4,
|
||||
active and friction_main_on, active and CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE,
|
||||
active and CS.out.standstill and actuators.longControlState == LongCtrlState.stopping, self.CP))
|
||||
else:
|
||||
self.apply_brake = 0
|
||||
dashboard_ready = (
|
||||
@@ -887,17 +873,6 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(gmcan.create_acc_dashboard_command(
|
||||
self.packer_pt, CanBus.POWERTRAIN, CC.enabled, hud_v_cruise * CV.MS_TO_KPH,
|
||||
hud_control, False, cruise_state=2, fcw_alert=fcw))
|
||||
if self.bolt_acc_brake_clock is not None:
|
||||
main_ns = getattr(CS, "bolt_pedal_main_ts_nanos", 0)
|
||||
main_ready = (CS.out.cruiseState.available and stock_ownership_clear and
|
||||
0 < main_ns <= now_nanos and now_nanos - main_ns <= 300_000_000)
|
||||
scheduled = self.bolt_acc_brake_clock.sample(
|
||||
now_nanos, getattr(CS, "bolt_acc_brake_clock_ns", 0), getattr(CS, "bolt_acc_brake_clock_counter", 0),
|
||||
active=bool(active), main_ready=main_ready, source_valid=bool(CS.out.canValid and not CS.out.canTimeout))
|
||||
if scheduled is not None:
|
||||
brake, counter, enabled, near_stop, full_stop = scheduled
|
||||
can_sends.append(gmcan.create_friction_brake_command(
|
||||
self.packer_ch, CanBus.POWERTRAIN, brake, counter, enabled, near_stop, full_stop, self.CP))
|
||||
if self.bolt_pedal_removed and self.frame % 100 == 0:
|
||||
can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN)
|
||||
# Stock ACC on the equipped variant is canceled while pedal control owns longitudinal.
|
||||
|
||||
@@ -62,7 +62,6 @@ class CarState(CarStateBase):
|
||||
self.cluster_min_speed = CV.KPH_TO_MS / 2.
|
||||
|
||||
self.loopback_lka_steering_cmd_updated = False
|
||||
self.loopback_lka_steering_cmd_counter = 0
|
||||
self.loopback_lka_steering_cmd_ts_nanos = 0
|
||||
self.pt_lka_steering_cmd_counter = 0
|
||||
self.cam_lka_steering_cmd_counter = 0
|
||||
@@ -76,8 +75,6 @@ class CarState(CarStateBase):
|
||||
self.pedal_sensor_counter = None
|
||||
self.pedal_packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) if CP.flags & GMFlags.PEDAL_LONG.value else None
|
||||
self.stock_acc_status_ts_nanos = 0
|
||||
self.bolt_acc_brake_clock_ns = 0
|
||||
self.bolt_acc_brake_clock_counter = 0
|
||||
self.bolt_pedal_removed_sources = ()
|
||||
self.bolt_pedal_removed_stock_ts_nanos = 0
|
||||
self.bolt_pedal_stock_active = False
|
||||
@@ -371,7 +368,6 @@ class CarState(CarStateBase):
|
||||
# Variables used for avoiding LKAS faults
|
||||
self.loopback_lka_steering_cmd_updated = len(loopback_cp.vl_all["ASCMLKASteeringCmd"]["RollingCounter"]) > 0
|
||||
if self.loopback_lka_steering_cmd_updated:
|
||||
self.loopback_lka_steering_cmd_counter = loopback_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"]
|
||||
self.loopback_lka_steering_cmd_ts_nanos = loopback_cp.ts_nanos["ASCMLKASteeringCmd"]["RollingCounter"]
|
||||
if (self.CP.networkLocation == NetworkLocation.fwdCamera and
|
||||
not (is_conventional_cc_pedal_profile(self.CP) and self.CP.flags & GMFlags.NO_CAMERA) and
|
||||
@@ -508,10 +504,6 @@ class CarState(CarStateBase):
|
||||
|
||||
# 1 - latched
|
||||
ret.seatbeltUnlatched = pt_cp.vl["BCMDoorBeltStatus"]["LeftSeatBelt"] == 0
|
||||
if int(self.CP.safetyConfigs[0].safetyParam) == 0x1CD and is_bolt_pedal_profile(self.CP):
|
||||
self.bolt_acc_brake_clock_ns = cam_cp.ts_nanos["EBCMFrictionBrakeCmd"]["RollingCounter"]
|
||||
self.bolt_acc_brake_clock_counter = int(cam_cp.vl["EBCMFrictionBrakeCmd"]["RollingCounter"])
|
||||
|
||||
ret.leftBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 1
|
||||
ret.rightBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 2
|
||||
|
||||
@@ -886,9 +878,6 @@ class CarState(CarStateBase):
|
||||
if hybrid is not None:
|
||||
cam_messages = [] if hybrid.removed else [("ASCMLKASteeringCmd", 10), ("AEBCmd", 10)]
|
||||
|
||||
if int(CP.safetyConfigs[0].safetyParam) == 0x1CD and is_bolt_pedal_profile(CP):
|
||||
cam_messages.append(("EBCMFrictionBrakeCmd", 25))
|
||||
|
||||
gps = get_car_gps_config(CP)
|
||||
if gps is not None:
|
||||
pt_messages += [(name, float("nan")) for name in gps.messages]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Conservative GM query-family and legacy CAN alias discovery."""
|
||||
"""Resolve one legacy Volt CAN alias using its observed PT-only source graph."""
|
||||
from opendbc.car.gm.values import CAR, CanBus
|
||||
from opendbc.car.fingerprints import all_legacy_fingerprint_cars, eliminate_incompatible_cars
|
||||
from opendbc.car.can_definitions import CanData
|
||||
|
||||
|
||||
def automatic_volt_candidate(candidate, fingerprint, candidates):
|
||||
@@ -26,41 +24,3 @@ def automatic_volt_candidate(candidate, fingerprint, candidates):
|
||||
# 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
|
||||
|
||||
|
||||
# Frequent normal-bus frames observed on mirrored pedal installations. This is
|
||||
# only diagnostic query-family evidence, never a vehicle/control fingerprint.
|
||||
PEDAL_NORMAL_BUS_LAYOUT = {0xD3: 2, 0xBE: 6, 0xC9: 8, 0x1C4: 8,
|
||||
0xF1: 6, 0x1E1: 7, 0x201: 6}
|
||||
|
||||
|
||||
def pedal_normal_bus0_topology(fingerprint):
|
||||
if any(messages for bus, messages in fingerprint.items() if bus not in (0, 2)):
|
||||
return False
|
||||
return all(fingerprint.get(bus, {}).get(address) == size
|
||||
for bus in (0, 2) for address, size in PEDAL_NORMAL_BUS_LAYOUT.items())
|
||||
|
||||
|
||||
def normal_bus0_topology(fingerprint):
|
||||
"""Recognize the GM query family without choosing a vehicle or actuator owner."""
|
||||
if pedal_normal_bus0_topology(fingerprint):
|
||||
return True
|
||||
pt = {address: size for address, size in fingerprint.get(CanBus.POWERTRAIN, {}).items()
|
||||
if address < 0x800 and address not in (0x7DF, 0x7E0, 0x7E8)}
|
||||
if len(pt) < 8:
|
||||
return False
|
||||
# Common Global A driver/steering state is shared by gateway, ASCM, SDGM,
|
||||
# camera, CC and interceptor installations. Camera presence and pedal presence
|
||||
# are deliberately not query-family requirements. Both ECM DLCs are in GM's
|
||||
# existing legacy inventory; they do not select a parser or control profile.
|
||||
if (pt.get(0xC9) in (6, 8) and pt.get(0x184) == 8 and
|
||||
pt.get(0x1C4) == 8 and pt.get(0x1E1) == 7):
|
||||
return True
|
||||
# Older GM architectures use different state messages. Reuse the existing
|
||||
# all-brand CAN inventory only when every surviving identity belongs to GM.
|
||||
candidates = all_legacy_fingerprint_cars()
|
||||
for address, size in pt.items():
|
||||
candidates = eliminate_incompatible_cars(CanData(address, bytes(size), CanBus.POWERTRAIN), candidates)
|
||||
if not candidates:
|
||||
return False
|
||||
return bool(candidates) and all(candidate in CAR for candidate in candidates)
|
||||
|
||||
@@ -41,9 +41,8 @@ class CarInterface(CarInterfaceBase):
|
||||
structs.CarState.GearShifter.eco, structs.CarState.GearShifter.manumatic)
|
||||
|
||||
@classmethod
|
||||
def get_params(cls, candidate, fingerprint, car_fw, alpha_long, is_release, docs, *, auto_pedal=True):
|
||||
selected = pedal_candidate(candidate, fingerprint) if auto_pedal else candidate
|
||||
return super().get_params(selected, fingerprint, car_fw, alpha_long, is_release, docs)
|
||||
def get_params(cls, candidate, fingerprint, car_fw, alpha_long, is_release, docs):
|
||||
return super().get_params(pedal_candidate(candidate, fingerprint), fingerprint, car_fw, alpha_long, is_release, docs)
|
||||
|
||||
def apply(self, c, now_nanos=None, *, startup_keepalive=False):
|
||||
if startup_keepalive:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from opendbc.car.pedal import supported_pedal_detected
|
||||
from opendbc.car.gm.values import CAR, CanBus
|
||||
from opendbc.car.vin import is_valid_vin
|
||||
|
||||
|
||||
def pedal_candidate(candidate, fingerprint):
|
||||
@@ -10,38 +9,14 @@ def pedal_candidate(candidate, fingerprint):
|
||||
return candidate
|
||||
|
||||
|
||||
def automatic_bolt_candidate(candidate, fingerprint, camera_cruise_messages, vin=None):
|
||||
"""Refine Bolt CAN/FW aliases using VIN year and live camera capability."""
|
||||
camera_cruise_states = {payload[1] & 0x7 for payload in camera_cruise_messages if len(payload) == 6}
|
||||
def automatic_bolt_candidate(candidate, fingerprint, camera_cruise_states):
|
||||
"""Correct the legacy Gen2 CAN alias only with observed stock ACC evidence."""
|
||||
bolt_ids = (CAR.CHEVROLET_BOLT_EUV, CAR.CHEVROLET_BOLT_CC_2017,
|
||||
CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2022_2023,
|
||||
CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL)
|
||||
if candidate is not None and candidate not in bolt_ids:
|
||||
if candidate not in bolt_ids:
|
||||
return candidate
|
||||
pt = fingerprint.get(CanBus.POWERTRAIN, {})
|
||||
# Year alone is not vehicle identity. Require the Bolt EV/EUV VIN descriptor
|
||||
# and matching generation's live PT layout before recovering a CAN collision.
|
||||
bolt_vin = (isinstance(vin, str) and is_valid_vin(vin) and
|
||||
vin.startswith('1G1F') and vin[5:8] == '6S0')
|
||||
if bolt_vin and vin[9] in 'HJKLMNP':
|
||||
if vin[9] in 'HJKLM':
|
||||
if any(pt.get(address) != length for address, length in ((0xD3, 2), (0xBE, 6), (0x130, 1))):
|
||||
return None
|
||||
return CAR.CHEVROLET_BOLT_CC_2017 if vin[9] == 'H' else CAR.CHEVROLET_BOLT_CC_2018_2021
|
||||
if any(pt.get(address) != length for address, length in
|
||||
((0xD3, 3), (0xBE, 7), (0x130, 3), (0x1CA, 5), (0x236, 8))):
|
||||
return None
|
||||
pedal = supported_pedal_detected(fingerprint, CanBus.POWERTRAIN, supported=True)
|
||||
if 0x201 in pt and not pedal:
|
||||
return None
|
||||
if camera_cruise_states and camera_cruise_states <= {2, 3}:
|
||||
return CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL if pedal else CAR.CHEVROLET_BOLT_ACC_2022_2023
|
||||
if camera_cruise_messages == {bytes(6)}:
|
||||
return CAR.CHEVROLET_BOLT_CC_2022_2023
|
||||
# Missing camera or non-adaptive ACC mode is not proof of ordinary cruise.
|
||||
return None
|
||||
if candidate is None:
|
||||
return None
|
||||
pt = fingerprint[CanBus.POWERTRAIN]
|
||||
if pt.get(0xD3) != 3:
|
||||
return candidate
|
||||
# Original Gen2 signature, qualified against the observed route's full DLCs.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.values import (GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal,
|
||||
is_volt_ascm_stock, is_volt_sdgm_accepted_envelope)
|
||||
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
|
||||
@@ -9,7 +8,7 @@ ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
def monitored_profile(CP: structs.CarParams) -> bool:
|
||||
return ((is_volt_ascm_longitudinal(CP) or is_volt_ascm_stock(CP) or is_volt_sdgm_accepted_envelope(CP)) 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)
|
||||
|
||||
|
||||
@@ -199,8 +199,8 @@ class TestAscmIntercept(unittest.TestCase):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0 if tick < 65 else 1)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
self.assertTrue(CarState(params(CAR.CHEVROLET_VOLT_ASCM)).steering_authority.enabled)
|
||||
for cp in (params(CAR.CHEVROLET_VOLT), params(CAR.CHEVROLET_BOLT_EUV)):
|
||||
for cp in (params(CAR.CHEVROLET_VOLT_ASCM), params(CAR.CHEVROLET_VOLT),
|
||||
params(CAR.CHEVROLET_BOLT_EUV)):
|
||||
self.assertFalse(CarState(cp).steering_authority.enabled)
|
||||
|
||||
def test_volt_steering_neutral_reacquisition(self):
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
"""Automatic pedal discovery must not move healthy normal CAN onto OBD pins."""
|
||||
from dataclasses import replace
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import car_helpers, fw_versions
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.ford.discovery import normal_bus_query_brand
|
||||
from opendbc.car.gm.discovery import pedal_normal_bus0_topology
|
||||
from opendbc.car.gm.values import CAR
|
||||
from opendbc.car.hyundai.discovery import NORMAL_BUS_LAYOUT, normal_bus_topology
|
||||
|
||||
# Original pre-query samples from a8c036520b19999c/00000004--7e3abadb55.
|
||||
# Query family only: live VIN/FW and ordinary CAN discovery still own identity.
|
||||
RECORDED = ((0, 211, '2d1c'),
|
||||
(2, 211, '2d1c'),
|
||||
(0, 513, '047b025258c6'),
|
||||
(2, 513, '047b025258c6'),
|
||||
(0, 190, '0402620001fd'),
|
||||
(2, 190, '0402620001fd'),
|
||||
(0, 201, '8000002749001000'),
|
||||
(2, 201, '8000002749001000'),
|
||||
(0, 452, '1362c1ee004901b7'),
|
||||
(2, 452, '1362c1ee004901b7'),
|
||||
(0, 241, '1c020041fbfe'),
|
||||
(2, 241, '1c020041fbfe'),
|
||||
(0, 481, '00000000031cc0'),
|
||||
(2, 481, '00000000031cc0'))
|
||||
|
||||
|
||||
def recorded_frames():
|
||||
return [CanData(address, bytes.fromhex(data), bus) for bus, address, data in RECORDED]
|
||||
|
||||
|
||||
def brand(frames):
|
||||
return normal_bus_query_brand(lambda **kwargs: [frames], normal_bus_topology,
|
||||
gm_probe=pedal_normal_bus0_topology)
|
||||
|
||||
|
||||
def test_recorded_pedal_layout_scopes_queries_to_gm():
|
||||
assert brand(recorded_frames()) == 'gm'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('missing', range(len(RECORDED)))
|
||||
def test_incomplete_layout_does_not_scope_gm(missing):
|
||||
frames = recorded_frames()
|
||||
del frames[missing]
|
||||
assert brand(frames) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad', range(len(RECORDED)))
|
||||
def test_wrong_bus_or_dlc_does_not_scope_gm(bad):
|
||||
for bus_delta, size_delta in ((1, 0), (128, 0), (0, -1), (0, 1)):
|
||||
frames = recorded_frames()
|
||||
original = frames[bad]
|
||||
frames[bad] = CanData(original.address, bytes(len(original.dat) + size_delta), original.src + bus_delta)
|
||||
assert brand(frames) is None
|
||||
|
||||
|
||||
def test_conflicting_dlc_cannot_be_hidden_by_later_valid_frame():
|
||||
frames = recorded_frames()
|
||||
bad = CanData(frames[0].address, bytes(len(frames[0].dat) + 1), frames[0].src)
|
||||
assert brand([bad, *frames]) is None
|
||||
|
||||
|
||||
def test_other_bus_activity_and_unrecognized_layout_keep_generic_discovery():
|
||||
assert brand([*recorded_frames(), CanData(123, bytes(8), 1)]) is None
|
||||
assert brand([]) is None
|
||||
assert brand([CanData(123, bytes(8), 0)]) is None
|
||||
|
||||
|
||||
def test_hyundai_path_and_existing_read_budget_are_preserved():
|
||||
frames = [CanData(address, bytes(size), bus) for (bus, address), size in NORMAL_BUS_LAYOUT.items()]
|
||||
assert brand(frames) == 'hyundai'
|
||||
calls = []
|
||||
|
||||
def recv(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return []
|
||||
assert normal_bus_query_brand(recv, lambda recv: normal_bus_topology(recv, clock=lambda: 1.),
|
||||
gm_probe=pedal_normal_bus0_topology) is None
|
||||
assert len(calls) == 24
|
||||
|
||||
|
||||
@pytest.mark.parametrize('registry', ['configured', 'effective'])
|
||||
@pytest.mark.parametrize('change', ['bus1', 'empty'])
|
||||
def test_changed_gm_query_topology_does_not_scope_automatic_queries(registry, change):
|
||||
requests = fw_versions.FW_QUERY_CONFIGS['gm'].requests
|
||||
if registry == 'configured':
|
||||
target, name = fw_versions.FW_QUERY_CONFIGS['gm'], 'requests'
|
||||
changed = [replace(r, bus=1) for r in requests] if change == 'bus1' else []
|
||||
else:
|
||||
target, name = fw_versions, 'REQUESTS'
|
||||
changed = [(b, c, replace(r, bus=1) if b == 'gm' else r) for b, c, r in fw_versions.REQUESTS] if change == 'bus1' else [
|
||||
item for item in fw_versions.REQUESTS if item[0] != 'gm']
|
||||
with mock.patch.object(target, name, changed):
|
||||
assert not car_helpers._gm_fw_uses_normal_bus0()
|
||||
with mock.patch.object(car_helpers, 'normal_bus_query_brand', return_value=None) as probe, \
|
||||
mock.patch.object(car_helpers, 'get_vin', return_value=(0, 0, '00000000000000000')), \
|
||||
mock.patch.object(car_helpers, 'get_present_ecus', return_value=set()), \
|
||||
mock.patch.object(car_helpers, 'get_fw_versions_ordered', return_value=[]), \
|
||||
mock.patch.object(car_helpers, 'can_fingerprint', return_value=(None, {})), \
|
||||
mock.patch.dict(car_helpers.os.environ, {'FINGERPRINT': '', 'SKIP_FW_QUERY': '', 'DISABLE_FW_CACHE': ''}):
|
||||
car_helpers.fingerprint(lambda **kw: [], mock.Mock(), mock.Mock(), None)
|
||||
assert probe.call_args.kwargs['gm_probe'] is None
|
||||
|
||||
|
||||
# Complete observed pre-query address/DLC set; payloads are not identity evidence.
|
||||
RECORDED_LAYOUT = {188: 8,
|
||||
320: 3,
|
||||
211: 2,
|
||||
390: 7,
|
||||
352: 5,
|
||||
513: 6,
|
||||
485: 8,
|
||||
170: 8,
|
||||
190: 6,
|
||||
201: 8,
|
||||
417: 7,
|
||||
493: 8,
|
||||
419: 1,
|
||||
495: 4,
|
||||
451: 8,
|
||||
499: 3,
|
||||
452: 8,
|
||||
503: 1,
|
||||
453: 6,
|
||||
193: 8,
|
||||
197: 8,
|
||||
209: 7,
|
||||
454: 8,
|
||||
241: 6,
|
||||
456: 8,
|
||||
463: 3,
|
||||
508: 8,
|
||||
500: 6,
|
||||
532: 6,
|
||||
554: 3,
|
||||
568: 1,
|
||||
647: 3,
|
||||
717: 5,
|
||||
707: 8,
|
||||
872: 1,
|
||||
967: 4,
|
||||
298: 8,
|
||||
977: 8,
|
||||
979: 7,
|
||||
985: 5,
|
||||
988: 6,
|
||||
1001: 8,
|
||||
1221: 5,
|
||||
869: 4,
|
||||
189: 7,
|
||||
407: 7,
|
||||
479: 3,
|
||||
501: 8,
|
||||
1257: 6,
|
||||
546: 7,
|
||||
550: 8,
|
||||
558: 8,
|
||||
384: 4,
|
||||
489: 8,
|
||||
560: 8,
|
||||
562: 8,
|
||||
711: 6,
|
||||
309: 8,
|
||||
481: 7,
|
||||
1322: 6,
|
||||
381: 6,
|
||||
840: 5,
|
||||
842: 5,
|
||||
566: 6,
|
||||
989: 8,
|
||||
386: 8,
|
||||
311: 8,
|
||||
308: 4,
|
||||
1249: 8,
|
||||
1922: 7,
|
||||
761: 7,
|
||||
866: 4,
|
||||
313: 8,
|
||||
1300: 8,
|
||||
995: 7,
|
||||
528: 5,
|
||||
844: 8,
|
||||
1907: 7,
|
||||
810: 8,
|
||||
353: 3,
|
||||
564: 5,
|
||||
608: 8,
|
||||
609: 6,
|
||||
610: 6,
|
||||
611: 6,
|
||||
612: 8,
|
||||
1017: 8,
|
||||
613: 8,
|
||||
1019: 2,
|
||||
1328: 4,
|
||||
1020: 8,
|
||||
961: 8,
|
||||
322: 7,
|
||||
753: 5,
|
||||
497: 8,
|
||||
388: 8,
|
||||
1105: 6,
|
||||
969: 8,
|
||||
800: 6,
|
||||
1005: 6,
|
||||
1022: 1,
|
||||
1013: 3,
|
||||
567: 5,
|
||||
1009: 8,
|
||||
2020: 8,
|
||||
2028: 8,
|
||||
1217: 8,
|
||||
1227: 4,
|
||||
1223: 3,
|
||||
1233: 8,
|
||||
1275: 3,
|
||||
1243: 3,
|
||||
1225: 7,
|
||||
1913: 7,
|
||||
1187: 4,
|
||||
1927: 7,
|
||||
1265: 8,
|
||||
1906: 7,
|
||||
1905: 7,
|
||||
1904: 7,
|
||||
328: 1,
|
||||
1912: 7,
|
||||
573: 1,
|
||||
1280: 4,
|
||||
304: 1,
|
||||
565: 5,
|
||||
563: 5}
|
||||
|
||||
|
||||
VIN = "1G1RC6S50HU000001"
|
||||
|
||||
|
||||
class RecordedIsoTpEcu:
|
||||
"""Actual query/UDS framing, inert CAN peer. No real bus or firmware claim."""
|
||||
def __init__(self, response_bus=0):
|
||||
self.clock = 0.
|
||||
self.frames = []
|
||||
self.pending = {}
|
||||
self.sent = []
|
||||
self.response_bus = response_bus
|
||||
|
||||
def send(self, messages):
|
||||
self.sent.extend(messages)
|
||||
for message in messages:
|
||||
payload = message.dat
|
||||
if payload[0] >> 4 == 3:
|
||||
self.frames.extend(self.pending.pop(message.address, []))
|
||||
continue
|
||||
if payload[0] >> 4 != 0:
|
||||
continue
|
||||
request = payload[1:1 + payload[0]]
|
||||
if message.address in (0x7df, 0x7e0) and request == b'\x22\xf1\x90':
|
||||
self.reply(0x7e0, 0x7e8, b'\x62\xf1\x90' + VIN.encode())
|
||||
elif request == b'\x3e' and message.address < 0x700:
|
||||
self.reply(message.address, message.address + 0x400, b'\x7e')
|
||||
elif request[:1] == b'\x1a' and message.address < 0x700:
|
||||
self.reply(message.address, message.address + 0x400, b'\x5a' + request[1:2] + b'FW')
|
||||
|
||||
def reply(self, tx, rx, value):
|
||||
if len(value) < 8:
|
||||
self.frames.append(CanData(rx, bytes([len(value)]) + value + bytes(7 - len(value)), self.response_bus))
|
||||
else:
|
||||
self.frames.append(CanData(rx, bytes([0x10 | len(value) >> 8, len(value) & 0xff]) + value[:6], self.response_bus))
|
||||
self.pending[tx] = [CanData(rx, bytes([0x20 | (i + 1) % 16]) + value[start:start + 7] + bytes(max(0, 7 - len(value[start:start + 7]))), self.response_bus)
|
||||
for i, start in enumerate(range(6, len(value), 7))]
|
||||
|
||||
def recv(self, wait_for_one=False):
|
||||
self.clock += .01
|
||||
values, self.frames = self.frames, []
|
||||
background = [CanData(address, bytes(size), bus) for bus in (0, 2) for address, size in RECORDED_LAYOUT.items()]
|
||||
return [values + background]
|
||||
|
||||
|
||||
def test_actual_automatic_vin_firmware_and_can_fingerprint_stay_normal_bus0():
|
||||
peer = RecordedIsoTpEcu()
|
||||
callbacks = []
|
||||
with mock.patch.dict(car_helpers.os.environ, {'FINGERPRINT': '', 'SKIP_FW_QUERY': '', 'DISABLE_FW_CACHE': ''}), \
|
||||
mock.patch.object(car_helpers.time, 'monotonic', side_effect=lambda: peer.clock), \
|
||||
mock.patch.object(car_helpers.time, 'sleep', side_effect=lambda seconds: setattr(peer, 'clock', peer.clock + seconds)):
|
||||
result = car_helpers.fingerprint(peer.recv, peer.send, callbacks.append, None)
|
||||
assert result[0] == CAR.CHEVROLET_BOLT_CC_2017
|
||||
assert result[2] == VIN
|
||||
assert result[3] and all(fw.brand == 'gm' and fw.bus == 0 for fw in result[3])
|
||||
assert result[4] == car_helpers.CarParams.FingerprintSource.can
|
||||
assert callbacks and set(callbacks) == {False}
|
||||
assert peer.sent and all(frame.src == 0 for frame in peer.sent)
|
||||
assert any(frame.dat[0] >> 4 == 3 for frame in peer.sent)
|
||||
|
||||
|
||||
def test_automatic_query_scope_preserves_actual_pedal_factory_safety_configuration():
|
||||
peer = RecordedIsoTpEcu()
|
||||
with mock.patch.dict(car_helpers.os.environ, {'FINGERPRINT': '', 'SKIP_FW_QUERY': '', 'DISABLE_FW_CACHE': ''}), \
|
||||
mock.patch.object(car_helpers.time, 'monotonic', side_effect=lambda: peer.clock), \
|
||||
mock.patch.object(car_helpers.time, 'sleep', side_effect=lambda seconds: setattr(peer, 'clock', peer.clock + seconds)):
|
||||
ci = car_helpers.get_car(peer.recv, peer.send, mock.Mock(), True, False)
|
||||
assert ci.CP.carFingerprint == CAR.CHEVROLET_BOLT_CC_2017
|
||||
assert ci.CP.openpilotLongitudinalControl
|
||||
assert ci.CP.safetyConfigs[0].safetyModel == car_helpers.CarParams.SafetyModel.gm
|
||||
assert ci.CP.safetyConfigs[0].safetyParam == 189
|
||||
# AOL is added by card's pre-create hook, not by diagnostic query selection.
|
||||
direct = car_helpers.interfaces[CAR.CHEVROLET_BOLT_CC_2017].get_params(
|
||||
CAR.CHEVROLET_BOLT_CC_2017, {0: RECORDED_LAYOUT, 1: {}, 2: RECORDED_LAYOUT}, [], True, False, docs=False)
|
||||
assert ci.CP.alternativeExperience == direct.alternativeExperience
|
||||
assert ci.CP.flags == direct.flags
|
||||
assert ci.CP.lateralTuning.to_dict() == direct.lateralTuning.to_dict()
|
||||
@@ -40,7 +40,6 @@ class TestBoltAccPedalFriction(unittest.TestCase):
|
||||
control.longActive = True
|
||||
control.actuators.accel = -3.
|
||||
state = structs.CarState()
|
||||
state.canValid = True
|
||||
state.vEgo = 12.
|
||||
state.gearShifter = structs.CarState.GearShifter.low
|
||||
state.cruiseState.available = True
|
||||
@@ -53,26 +52,12 @@ class TestBoltAccPedalFriction(unittest.TestCase):
|
||||
buttons_counter=0,
|
||||
pscm_status=dict.fromkeys(("HandsOffSWDetectionMode", "HandsOffSWlDetectionStatus", "LKATorqueDeliveredStatus",
|
||||
"LKADriverAppldTrq", "LKATorqueDelivered", "LKATotalTorqueDelivered", "RollingCounter", "PSCMStatusChecksum"), 0))
|
||||
from opendbc.car.gm.steering_authority import SteeringAuthority
|
||||
cs.steering_authority = SteeringAuthority(cp)
|
||||
return controller, control, state, cs
|
||||
|
||||
def step(self, fixture, frame, now=1_000_000_000):
|
||||
controller, control, state, cs = fixture
|
||||
controller.frame = frame
|
||||
now += (frame - 4) * 10_000_000
|
||||
# Advance the declared healthy physical receipts with the 100 Hz fixture.
|
||||
# Explicitly stale/absent receipts supplied by each test stay stale/absent.
|
||||
previous = getattr(cs, '_healthy_fixture_stamps', {})
|
||||
for name in ('pedal_sensor_ts_nanos', 'stock_acc_status_ts_nanos', 'bolt_pedal_gear_ts_nanos', 'bolt_pedal_main_ts_nanos'):
|
||||
value = getattr(cs, name)
|
||||
if value == previous.get(name, 950_000_000):
|
||||
setattr(cs, name, now - 50_000_000)
|
||||
previous[name] = now - 50_000_000
|
||||
cs._healthy_fixture_stamps = previous
|
||||
cs.out = state.as_reader()
|
||||
cs.bolt_acc_brake_clock_ns = now
|
||||
cs.bolt_acc_brake_clock_counter = (frame // 4) % 4
|
||||
_, messages = controller.update(control.as_reader(), cs, now)
|
||||
return messages
|
||||
|
||||
@@ -265,80 +250,3 @@ class TestBoltAccPedalFriction(unittest.TestCase):
|
||||
payloads = {m[0]: m[1].hex() for m in messages if m[0] in (0x200, 0x1F5, 0xBD, 0x315)}
|
||||
self.assertEqual(payloads, {0x200: "056f02b888ce", 0x1F5: "0c0c000500020100",
|
||||
0xBD: "20000000000000", 0x315: "9000700000"})
|
||||
|
||||
|
||||
class TestBoltAccBrakeClock(unittest.TestCase):
|
||||
def test_clock_is_fresh_nonrepeating_and_never_expands_authority(self):
|
||||
from opendbc.car.gm.bolt_acc_brake_clock import BoltAccBrakeClock
|
||||
clock = BoltAccBrakeClock()
|
||||
clock.prepare(1_000_000_000, 24, True, False, False)
|
||||
|
||||
def sample(now, source, counter, **changes):
|
||||
flags = dict(active=True, main_ready=True, source_valid=True)
|
||||
flags.update(changes)
|
||||
return clock.sample(now, source, counter, **flags)
|
||||
self.assertEqual(sample(1_010_000_000, 1_005_000_000, 2), (24, 2, True, False, False))
|
||||
self.assertIsNone(sample(1_020_000_000, 1_005_000_000, 2))
|
||||
self.assertIsNone(sample(1_020_000_000, 1_015_000_000, 2))
|
||||
self.assertIsNone(sample(1_020_000_000, 1_021_000_000, 3))
|
||||
self.assertIsNone(sample(1_020_000_000, 0, 3))
|
||||
self.assertIsNone(sample(1_020_000_000, 1_015_000_000, 3, source_valid=False))
|
||||
self.assertEqual(sample(1_050_000_000, 1_045_000_000, 3, active=False), (0, 3, False, False, False))
|
||||
self.assertIsNone(sample(1_090_000_000, 1_085_000_000, 0))
|
||||
clock.prepare(1_090_000_000, 24, True, False, False)
|
||||
self.assertIsNone(sample(1_090_000_000, 1_000_000_000, 0))
|
||||
self.assertIsNone(sample(1_090_000_000, 1_085_000_000, 0, main_ready=False))
|
||||
self.assertIsNone(sample(1_090_000_000, 1_085_000_000, 0))
|
||||
|
||||
def test_controller_emits_on_factory_cycle_between_pedal_slots(self):
|
||||
fixture = TestBoltAccPedalFriction().fixture()
|
||||
controller, control, state, cs = fixture
|
||||
state.canValid = True
|
||||
cs.out = state.as_reader()
|
||||
cs.bolt_acc_brake_clock_ns = 995_000_000
|
||||
cs.bolt_acc_brake_clock_counter = 2
|
||||
controller.frame = 4
|
||||
_, first = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual(brake_fields(brake_frames(first)[0])[2], 2)
|
||||
pedal_first = [m for m in first if m[0] == 0x200]
|
||||
self.assertEqual(len(pedal_first), 1)
|
||||
controller.frame = 5
|
||||
_, duplicate = controller.update(control.as_reader(), cs, 1_010_000_000)
|
||||
self.assertFalse(brake_frames(duplicate))
|
||||
controller.frame = 8
|
||||
_, waiting = controller.update(control.as_reader(), cs, 1_040_000_000)
|
||||
self.assertFalse(brake_frames(waiting))
|
||||
cs.bolt_acc_brake_clock_ns = 1_045_000_000
|
||||
cs.bolt_acc_brake_clock_counter = 3
|
||||
controller.frame = 9
|
||||
_, next_cycle = controller.update(control.as_reader(), cs, 1_050_000_000)
|
||||
self.assertEqual(brake_fields(brake_frames(next_cycle)[0])[2], 3)
|
||||
self.assertFalse(any(m[0] == 0x200 for m in next_cycle))
|
||||
self.assertFalse(any(m[0] in (0x2CB, 0xBD, 0x1F5) for m in next_cycle))
|
||||
|
||||
def test_scope_excludes_removed_camera_and_no_acc(self):
|
||||
from opendbc.car.gm.tests.test_bolt_pedal import params
|
||||
for identity in (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021,
|
||||
CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL):
|
||||
cp = params(identity, pedal=True, removed=True)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
self.assertIsNone(controller.bolt_acc_brake_clock)
|
||||
|
||||
def test_lost_or_frozen_clock_releases_once_and_cannot_rearm_itself(self):
|
||||
from opendbc.car.gm.bolt_acc_brake_clock import BoltAccBrakeClock
|
||||
for failure in ('stale', 'frozen', 'invalid'):
|
||||
clock = BoltAccBrakeClock()
|
||||
clock.prepare(1_000_000_000, 24, True, False, False)
|
||||
self.assertEqual(clock.sample(1_000_000_000, 1_000_000_000, 2,
|
||||
active=True, main_ready=True, source_valid=True), (24, 2, True, False, False))
|
||||
clock.prepare(1_090_000_000, 24, True, False, False)
|
||||
source = 1_000_000_000 if failure == 'stale' else 1_090_000_000
|
||||
self.assertEqual(clock.sample(1_090_000_000, source, 2,
|
||||
active=True, main_ready=True, source_valid=failure != 'invalid'), (0, 3, False, False, False))
|
||||
self.assertIsNone(clock.sample(1_100_000_000, source, 2, active=True, main_ready=True, source_valid=True))
|
||||
clock.prepare(1_130_000_000, 24, True, False, False)
|
||||
self.assertIsNone(clock.sample(1_130_000_000, 1_130_000_000, 3,
|
||||
active=True, main_ready=True, source_valid=True))
|
||||
clock.prepare(1_170_000_000, 24, True, False, False)
|
||||
self.assertEqual(clock.sample(1_170_000_000, 1_170_000_000, 0,
|
||||
active=True, main_ready=True, source_valid=True), (24, 0, True, False, False))
|
||||
|
||||
@@ -371,43 +371,25 @@ class TestBoltPedalMessages(unittest.TestCase):
|
||||
cam_lka_steering_cmd_counter=0, loopback_lka_steering_cmd_updated=False,
|
||||
loopback_lka_steering_cmd_ts_nanos=1_000_000_000, pt_lka_steering_cmd_counter=0,
|
||||
buttons_counter=0)
|
||||
state.canValid = int(cp.safetyConfigs[0].safetyParam) == 0x1CD
|
||||
cs.out = state.as_reader()
|
||||
cs.bolt_acc_brake_clock_ns = 995_000_000
|
||||
cs.bolt_acc_brake_clock_counter = 1
|
||||
|
||||
def step(tick, controller=controller, control=control, state=state, cs=cs):
|
||||
# Supply each 25 Hz controller slot with fresh physical receipts and
|
||||
# the corresponding factory brake cycle, rather than reusing time.
|
||||
now = 1_000_000_000 + (tick - 4) * 10_000_000
|
||||
controller.frame = tick
|
||||
cs.loopback_lka_steering_cmd_ts_nanos = now
|
||||
for name in ('pedal_sensor_ts_nanos', 'stock_acc_status_ts_nanos', 'bolt_pedal_gear_ts_nanos', 'bolt_pedal_main_ts_nanos'):
|
||||
setattr(cs, name, now - 50_000_000)
|
||||
cs.bolt_acc_brake_clock_ns = now - 5_000_000
|
||||
cs.bolt_acc_brake_clock_counter = (tick // 4) % 4
|
||||
cs.out = state.as_reader()
|
||||
return controller.update(control.as_reader(), cs, now)[1]
|
||||
|
||||
messages = step(4)
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
expected = ([0x3D1] if candidate in NO_ACC_BOLT_CAR else []) + [0x200, 0x1F5, 0xBD]
|
||||
if candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
|
||||
expected += [0x370, 0x315]
|
||||
expected.append(0x315)
|
||||
self.assertEqual([m[0] for m in messages], expected)
|
||||
self.assertGreater(controller.pedal_steady, 0.)
|
||||
first_fraction = controller.pedal_steady
|
||||
|
||||
controller.frame = 8
|
||||
control.actuators.accel = 2.
|
||||
messages = step(8)
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual([m[0] for m in messages], expected)
|
||||
self.assertGreater(controller.pedal_steady, first_fraction)
|
||||
self.assertLess(controller.pedal_steady - first_fraction, 0.06)
|
||||
|
||||
controller.frame = 12
|
||||
cs.pedal_sensor_healthy = False
|
||||
messages = step(12)
|
||||
self.assertEqual([m[0] for m in messages], [address for address in expected if address != 0x370])
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual([m[0] for m in messages], expected)
|
||||
payloads = {message[0]: message[1] for message in messages}
|
||||
self.assertEqual(payloads[0x200][0:4], b"\x00" * 4)
|
||||
self.assertEqual(payloads[0x1F5], b"\x0c\x0c\x00\x06\x00\x00\x01\x00")
|
||||
@@ -419,7 +401,7 @@ class TestBoltPedalMessages(unittest.TestCase):
|
||||
cs.pedal_sensor_healthy = True
|
||||
state.cruiseState.enabled = True
|
||||
cs.out = state.as_reader()
|
||||
messages = step(16)
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual(messages[0][1][0:4], b"\x00" * 4)
|
||||
self.assertIn((0x1E1, 2), [(m[0], m[2]) for m in messages])
|
||||
|
||||
@@ -492,8 +474,6 @@ class TestBoltPedalStartupParser(unittest.TestCase):
|
||||
if 'GAS_SENSOR' not in omit:
|
||||
frames.append(TestBoltPedalMessages.sensor(packer, 0, sensor_counter % 16, state=sensor_state))
|
||||
frames += [packer.make_can_msg('ASCMLKASteeringCmd', 2, {}), packer.make_can_msg('AEBCmd', 2, {})]
|
||||
if int(ci.CP.safetyConfigs[0].safetyParam) == 0x1CD and now % 40_000_000 < 10_000_000:
|
||||
frames.append(packer.make_can_msg('EBCMFrictionBrakeCmd', 2, {'RollingCounter': (now // 40_000_000) % 4}))
|
||||
if camera_stock:
|
||||
frames.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 2}))
|
||||
if bad_length is not None:
|
||||
@@ -794,8 +774,6 @@ class TestBoltPedalStartupParser(unittest.TestCase):
|
||||
camera = ['ASCMLKASteeringCmd', 'AEBCmd'] if camera_present else []
|
||||
if cp.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
|
||||
camera.append('ASCMActiveCruiseControlStatus')
|
||||
if int(cp.safetyConfigs[0].safetyParam) == 0x1CD:
|
||||
camera.append('EBCMFrictionBrakeCmd')
|
||||
sensor_counter = 0
|
||||
for index in range(250):
|
||||
frames = []
|
||||
@@ -803,7 +781,7 @@ class TestBoltPedalStartupParser(unittest.TestCase):
|
||||
for name in names:
|
||||
if (bus, name) == missing:
|
||||
continue
|
||||
rate = pt_messages[name] if bus == 0 else 25 if name in ('ASCMActiveCruiseControlStatus', 'EBCMFrictionBrakeCmd') else 10
|
||||
rate = pt_messages[name] if bus == 0 else 25 if name == 'ASCMActiveCruiseControlStatus' else 10
|
||||
if index and index * rate // 100 == (index - 1) * rate // 100:
|
||||
continue
|
||||
if name == 'ASCMSteeringButton':
|
||||
@@ -1116,7 +1094,7 @@ class TestBoltPaddleModes(unittest.TestCase):
|
||||
control.enabled = control.longActive = False
|
||||
frames.append((0x1E1, button_bytes(buttons, tick % 4), 0))
|
||||
frames.extend(packer.make_can_msg(name, 2, values.get(name, {})) for name in
|
||||
('ASCMLKASteeringCmd', 'AEBCmd', 'ASCMActiveCruiseControlStatus', 'EBCMFrictionBrakeCmd'))
|
||||
('ASCMLKASteeringCmd', 'AEBCmd', 'ASCMActiveCruiseControlStatus'))
|
||||
frames.append(packer.make_can_msg('ASCMLKASteeringCmd', 128, {}))
|
||||
safety.reset_recorded_can()
|
||||
regen_observation_index = None
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
"""Recorded Bolt identity selection, capability evidence and explicit overrides."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import car_helpers, gen_empty_fingerprint, structs
|
||||
from opendbc.car.can_definitions import CanData, CanRecvCallable
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.pedal_capability import automatic_bolt_candidate
|
||||
from opendbc.car.gm.values import CAR, GMFlags
|
||||
|
||||
RECORDINGS = json.loads((Path(__file__).parent / 'testdata/bolt_vin_fingerprints.json').read_text())
|
||||
BOLTS = (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021,
|
||||
CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023,
|
||||
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL)
|
||||
|
||||
|
||||
def recording(index):
|
||||
row = RECORDINGS[index]
|
||||
fp = gen_empty_fingerprint()
|
||||
fp.update({int(bus): {int(address): size for address, size in messages.items()}
|
||||
for bus, messages in row['fingerprint'].items()})
|
||||
# Descriptor/year retained; check digit and serial are synthetic.
|
||||
vin = row['vinPrefix'] + '0' + row['year'] + '0000000'
|
||||
return fp, {bytes.fromhex(raw) for raw in row['cameraPayloads']}, vin
|
||||
|
||||
|
||||
def packets(fp, camera):
|
||||
return [CanData(address, bytes(size), bus) for bus, messages in fp.items()
|
||||
for address, size in messages.items() if not (bus == 2 and address == 0x370)] + [
|
||||
CanData(0x370, raw, 2) for raw in camera]
|
||||
|
||||
|
||||
def receiver(frames: list[CanData]) -> CanRecvCallable:
|
||||
def recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
||||
return [frames]
|
||||
return recv
|
||||
|
||||
|
||||
@pytest.mark.parametrize('index', range(4))
|
||||
def test_recorded_routes_select_correct_identity_through_can(index):
|
||||
fp, camera, vin = recording(index)
|
||||
selected, observed = car_helpers.can_fingerprint(receiver(packets(fp, camera)), vin=vin)
|
||||
assert selected == RECORDINGS[index]['expected']
|
||||
assert observed == fp
|
||||
|
||||
|
||||
@pytest.mark.parametrize('year,expected', [('H', BOLTS[0]), ('J', BOLTS[1]), ('K', BOLTS[1]),
|
||||
('L', BOLTS[1]), ('M', BOLTS[1])])
|
||||
@pytest.mark.parametrize('alias', [None, *BOLTS, CAR.CHEVROLET_BOLT_EUV])
|
||||
def test_gen1_year_corrects_all_automatic_aliases(year, expected, alias):
|
||||
fp, camera, vin = recording(0)
|
||||
vin = vin[:9] + year + vin[10:]
|
||||
assert automatic_bolt_candidate(alias, fp, camera, vin) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('year', ['N', 'P'])
|
||||
@pytest.mark.parametrize('pedal', [False, True])
|
||||
@pytest.mark.parametrize('acc', [False, True])
|
||||
@pytest.mark.parametrize('alias', [None, *BOLTS, CAR.CHEVROLET_BOLT_EUV])
|
||||
def test_gen2_capability_selects_all_three_identities(year, pedal, acc, alias):
|
||||
fp, camera, vin = recording(3 if acc else 2)
|
||||
vin = vin[:9] + year + vin[10:]
|
||||
fp[0].pop(0x201, None)
|
||||
if pedal:
|
||||
fp[0][0x201] = 6
|
||||
expected = BOLTS[4 if pedal else 3] if acc else BOLTS[2]
|
||||
assert automatic_bolt_candidate(alias, fp, camera, vin) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('camera', [set(), {b'\0\4\0\0\0\0'}, {b'\0\5\0\0\0\0'},
|
||||
{b'\1\0\0\0\0\0'}, {bytes(6), b'\0\2\0\0\0\0'}])
|
||||
def test_missing_nonadaptive_or_conflicting_camera_cannot_select_no_acc(camera):
|
||||
fp, _, vin = recording(2)
|
||||
assert automatic_bolt_candidate(BOLTS[1], fp, camera, vin) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('address', [0xD3, 0xBE, 0x130, 0x1CA, 0x236])
|
||||
def test_missing_generation_evidence_cannot_recover_unknown_candidate(address):
|
||||
fp, camera, vin = recording(3)
|
||||
del fp[0][address]
|
||||
assert automatic_bolt_candidate(None, fp, camera, vin) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('vin', ['', '0' * 17, '1G1FX6S00Z0000000', '1G1RA6S00P0000000',
|
||||
'1G1FX6S00P000000I', '1G1FX6S00P'])
|
||||
def test_invalid_nonbolt_or_unsupported_vin_cannot_recover_unknown_candidate(vin):
|
||||
fp, camera, _ = recording(3)
|
||||
assert automatic_bolt_candidate(None, fp, camera, vin) is None
|
||||
|
||||
|
||||
def test_wrong_bus_or_dlc_is_not_pedal_evidence():
|
||||
fp, camera, vin = recording(3)
|
||||
fp[0].pop(0x201)
|
||||
assert automatic_bolt_candidate(None, fp, camera, vin) == BOLTS[3]
|
||||
fp[0][0x201] = 7
|
||||
assert automatic_bolt_candidate(None, fp, camera, vin) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('candidate', [CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_MALIBU, 'HYUNDAI IONIQ 6 2023'])
|
||||
def test_other_identities_are_never_rewritten(candidate):
|
||||
fp, camera, vin = recording(3)
|
||||
assert automatic_bolt_candidate(candidate, fp, camera, vin) == candidate
|
||||
|
||||
|
||||
def fingerprint_with_firmware(fp, camera, vin, fw_candidate, fixed=''):
|
||||
cached = SimpleNamespace(brand='gm', carVin=vin, carFw=[object()])
|
||||
with patch.dict(car_helpers.os.environ, {'FINGERPRINT': fixed, 'SKIP_FW_QUERY': '', 'DISABLE_FW_CACHE': ''}), \
|
||||
patch.object(car_helpers, 'match_fw_to_car', return_value=(True, {fw_candidate})):
|
||||
return car_helpers.fingerprint(receiver(packets(fp, camera)), lambda _: None, lambda _: None, cached)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('index', range(4))
|
||||
def test_firmware_alias_cannot_undo_vin_refinement(index):
|
||||
fp, camera, vin = recording(index)
|
||||
result = fingerprint_with_firmware(fp, camera, vin, CAR.CHEVROLET_BOLT_EUV)
|
||||
assert result[0] == RECORDINGS[index]['expected']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('manual', BOLTS)
|
||||
def test_environment_override_wins_after_all_automatic_selection(manual):
|
||||
fp, camera, vin = recording(3)
|
||||
result = fingerprint_with_firmware(fp, camera, vin, BOLTS[1], fixed=manual)
|
||||
assert result[0] == manual
|
||||
assert result[4] == structs.CarParams.FingerprintSource.fixed
|
||||
|
||||
|
||||
@pytest.mark.parametrize('manual', BOLTS)
|
||||
@pytest.mark.parametrize('source', ['forced', 'environment'])
|
||||
def test_get_car_preserves_exact_manual_identity_and_safety(manual, source):
|
||||
fp, _, vin = recording(3)
|
||||
# Avoid starting Card/controller: build actual CP, capture its final selection.
|
||||
|
||||
class Factory:
|
||||
get_params = CarInterface.get_params
|
||||
|
||||
def __new__(cls, cp):
|
||||
return SimpleNamespace(CP=cp)
|
||||
|
||||
fingerprint = (BOLTS[4] if source == 'forced' else manual, fp, vin, [],
|
||||
structs.CarParams.FingerprintSource.can if source == 'forced' else structs.CarParams.FingerprintSource.fixed, True)
|
||||
with patch.object(car_helpers, 'fingerprint', return_value=fingerprint), \
|
||||
patch.dict(car_helpers.interfaces, {car: Factory for car in BOLTS}), \
|
||||
patch.dict(car_helpers.os.environ, {'FINGERPRINT': '' if source == 'forced' else manual}):
|
||||
car = car_helpers.get_car(receiver([]), lambda _: None, lambda _: None, False, False,
|
||||
forced_candidate=manual if source == 'forced' else None)
|
||||
expected = CarInterface.get_params(manual, fp, [], False, False, False, auto_pedal=False)
|
||||
assert car.CP.carFingerprint == manual
|
||||
assert [s.to_dict() for s in car.CP.safetyConfigs] == [s.to_dict() for s in expected.safetyConfigs]
|
||||
assert bool(car.CP.flags & GMFlags.PEDAL_LONG) == bool(expected.flags & GMFlags.PEDAL_LONG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('index,pedal,expected', [(0, True, BOLTS[0]), (1, True, BOLTS[1]),
|
||||
(2, True, BOLTS[2]), (3, False, BOLTS[3]), (3, True, BOLTS[4])])
|
||||
def test_automatic_selection_reaches_existing_safety_and_torque_profile(index, pedal, expected):
|
||||
from opendbc.car.gm.values import CarControllerParams
|
||||
fp, camera, vin = recording(index)
|
||||
fp[0].pop(0x201, None)
|
||||
if pedal:
|
||||
fp[0][0x201] = 6
|
||||
selected, _ = car_helpers.can_fingerprint(receiver(packets(fp, camera)), vin=vin)
|
||||
assert selected == expected
|
||||
cp = CarInterface.get_params(selected, fp, [], False, False, False)
|
||||
existing = CarInterface.get_params(expected, fp, [], False, False, False)
|
||||
assert cp.carFingerprint == expected
|
||||
assert [s.to_dict() for s in cp.safetyConfigs] == [s.to_dict() for s in existing.safetyConfigs]
|
||||
assert bool(cp.flags & GMFlags.PEDAL_LONG) == pedal
|
||||
assert CarControllerParams(cp).STEER_MAX == (450 if index == 0 else 300)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bus,size', [(0, 6), (128, 6), (130, 6), (2, 5), (2, 7)])
|
||||
def test_wrong_camera_bus_or_dlc_cannot_select_acc(bus, size):
|
||||
fp, camera, vin = recording(3)
|
||||
fp[2].pop(0x370, None)
|
||||
frames = packets(fp, set()) + [CanData(0x370, next(iter(camera))[:size].ljust(size, b'\0'), bus)]
|
||||
selected, _ = car_helpers.can_fingerprint(receiver(frames), vin=vin)
|
||||
assert selected is None
|
||||
@@ -65,62 +65,6 @@ def controller_messages(cp, frame, controller=None, *, stock_acc_enabled=False,
|
||||
|
||||
|
||||
class TestBoltVoltConfigurations(unittest.TestCase):
|
||||
def test_bolt_steering_counter_tracks_real_loopback_values(self):
|
||||
for identity in PEDAL_BOLT_CAR:
|
||||
for alpha in (False, True):
|
||||
for removed in (False, True):
|
||||
with self.subTest(identity=identity, alpha=alpha, removed=removed):
|
||||
cp = pedal_params(identity, setting=True, pedal=True, alpha_long=alpha, removed=removed)
|
||||
if identity == CAR.CHEVROLET_BOLT_CC_2018_2021 and not removed:
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x9D)
|
||||
cs = CarState(cp)
|
||||
parsers = cs.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
controller = CarController(DBC[identity], cp)
|
||||
control = structs.CarControl()
|
||||
for step, (received, expected) in enumerate((([3], 0), ([0, 1], 2), ([0, 0], 1),
|
||||
([1], 2), ([], 2), ([2, 3], 0), ([0], 1))):
|
||||
now = 1_000_000_000 + step * 100_000_000
|
||||
packets = [packer.make_can_msg("ASCMLKASteeringCmd", 128, {"RollingCounter": value}) for value in received]
|
||||
for parser in parsers.values():
|
||||
parser.update([(now - 20_000_000, packets)])
|
||||
cs.out = cs.update(parsers).as_reader()
|
||||
self.assertEqual(cs.loopback_lka_steering_cmd_updated, bool(received))
|
||||
controller.frame = 20 + step * 10
|
||||
_, commands = controller.update(control.as_reader(), cs, now)
|
||||
steering = [message for message in commands if message[0] == 0x180 and message[2] == 0]
|
||||
self.assertEqual(len(steering), 1)
|
||||
data = steering[0][1]
|
||||
self.assertEqual((data[0] >> 4) & 3, expected)
|
||||
self.assertFalse(data[0] & 8)
|
||||
|
||||
def test_nonpedal_gm_preserves_legacy_loopback_counter_progression(self):
|
||||
for identity in (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_BOLT_EUV):
|
||||
for alpha in (False, True):
|
||||
with self.subTest(identity=identity, alpha=alpha):
|
||||
cp = ordinary_params(identity, alpha=alpha, radar=True)
|
||||
cs = CarState(cp)
|
||||
parsers = cs.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
controller = CarController(DBC[identity], cp)
|
||||
self.assertFalse(controller.bolt_steering_ack_counter)
|
||||
control = structs.CarControl()
|
||||
expected = 0
|
||||
for step, received in enumerate(([3], [0, 1], [0, 0], [1], [], [2, 3], [0])):
|
||||
now = 1_000_000_000 + step * 100_000_000
|
||||
packets = [packer.make_can_msg("ASCMLKASteeringCmd", 128, {"RollingCounter": value}) for value in received]
|
||||
for parser in parsers.values():
|
||||
parser.update([(now - 20_000_000, packets)])
|
||||
cs.out = cs.update(parsers).as_reader()
|
||||
expected += bool(received)
|
||||
controller.frame = 20 + step * 10
|
||||
_, commands = controller.update(control.as_reader(), cs, now)
|
||||
steering = [message for message in commands if message[0] == 0x180 and message[2] == 0]
|
||||
self.assertEqual(len(steering), 1)
|
||||
self.assertEqual((steering[0][1][0] >> 4) & 3, expected % 4)
|
||||
self.assertFalse(steering[0][1][0] & 8)
|
||||
|
||||
def test_all_bolt_pedal_generations_withdraw_on_missing_stale_or_future_gear(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
for alpha in (False, True):
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
"""GM-wide normal wiring discovery; recorded Sierra and unchanged control owners."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from opendbc.car import car_helpers, gen_empty_fingerprint
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.fingerprints import _FINGERPRINTS
|
||||
from opendbc.car.ford.discovery import normal_bus_query_brand
|
||||
from opendbc.car.gm.discovery import normal_bus0_topology, pedal_normal_bus0_topology
|
||||
from opendbc.car.gm.fingerprints import FINGERPRINTS
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_automatic_discovery import RecordedIsoTpEcu
|
||||
from opendbc.car.gm.values import CAR
|
||||
from opendbc.car.hyundai.discovery import normal_bus_topology as hyundai_probe
|
||||
|
||||
SIERRA = json.loads((Path(__file__).parent / 'testdata/sierra_startup_discovery.json').read_text())
|
||||
GM_LAYOUTS = [(car, i, fp) for car, layouts in FINGERPRINTS.items() for i, fp in enumerate(layouts)]
|
||||
OTHER_LAYOUTS = [(car, i, fp) for car, layouts in _FINGERPRINTS.items() if car not in CAR for i, fp in enumerate(layouts)]
|
||||
|
||||
|
||||
def frames(fp):
|
||||
return [CanData(a, bytes(n), b) for b, messages in fp.items() for a, n in messages.items()]
|
||||
|
||||
|
||||
def sierra_fingerprint() -> dict[int, dict[int, int]]:
|
||||
fp = gen_empty_fingerprint()
|
||||
fp.update({int(b): {int(a): n for a, n in messages.items()} for b, messages in SIERRA['fingerprint'].items()})
|
||||
return fp
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car,index,layout', GM_LAYOUTS, ids=[f'{c}-{i}' for c, i, _ in GM_LAYOUTS])
|
||||
def test_all_registered_gm_can_layouts_use_normal_query_family(car, index, layout):
|
||||
assert normal_bus0_topology({0: layout})
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car,index,layout', OTHER_LAYOUTS, ids=[f'{c}-{i}' for c, i, _ in OTHER_LAYOUTS])
|
||||
def test_other_brand_layouts_do_not_select_gm_queries(car, index, layout):
|
||||
assert not normal_bus0_topology({0: layout})
|
||||
assert not normal_bus0_topology({0: layout, 2: layout})
|
||||
|
||||
|
||||
def test_recorded_sierra_passive_window_is_covered_without_pedal():
|
||||
batches = [[CanData(a, bytes(n), b) for a, n, b in packet] for packet in SIERRA['packets']]
|
||||
observed = {}
|
||||
for packet in batches:
|
||||
for frame in packet:
|
||||
observed.setdefault(frame.src, {})[frame.address] = len(frame.dat)
|
||||
assert not pedal_normal_bus0_topology(observed) # This was the uncovered path.
|
||||
assert normal_bus0_topology(observed)
|
||||
stream = iter(batches)
|
||||
reads = []
|
||||
clock = [0.]
|
||||
|
||||
def recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
||||
reads.append(wait_for_one)
|
||||
clock[0] += .01
|
||||
return [next(stream, [])]
|
||||
assert normal_bus_query_brand(recv, lambda read: hyundai_probe(read, clock=lambda: clock[0]),
|
||||
gm_probe=normal_bus0_topology, clock=lambda: clock[0]) == 'gm'
|
||||
assert len(reads) <= 13
|
||||
|
||||
|
||||
@pytest.mark.parametrize('case', ['empty', 'sparse', 'wrong_bus', 'loopback', 'wrong_dlc'])
|
||||
def test_unsupported_evidence_keeps_generic_discovery(case):
|
||||
fp = {0: {0xC9: 8, 0x184: 8, 0x1C4: 8, 0x1E1: 7}}
|
||||
if case == 'empty':
|
||||
fp = {}
|
||||
elif case in ('wrong_bus', 'loopback'):
|
||||
fp = {2 if case == 'wrong_bus' else 128: sierra_fingerprint()[0]}
|
||||
elif case == 'wrong_dlc':
|
||||
fp = {0: dict(sierra_fingerprint()[0])}
|
||||
fp[0][0x184] = 7
|
||||
assert not normal_bus0_topology(fp)
|
||||
|
||||
|
||||
def test_conflicting_dlc_does_not_fall_back_to_positive_gm_evidence():
|
||||
rows = frames(sierra_fingerprint())
|
||||
bad = CanData(0x184, bytes(7), 0)
|
||||
|
||||
def recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
||||
return [[bad, *rows]]
|
||||
assert normal_bus_query_brand(recv, hyundai_probe, gm_probe=normal_bus0_topology, clock=lambda: 0.) is None
|
||||
|
||||
|
||||
class SierraEcu(RecordedIsoTpEcu):
|
||||
def recv(self, wait_for_one=False):
|
||||
self.clock += .01
|
||||
values, self.frames = self.frames, []
|
||||
return [values + frames(sierra_fingerprint())]
|
||||
|
||||
|
||||
def discover(peer, *, legacy=False, instantiate=False):
|
||||
callbacks = []
|
||||
with mock.patch.dict(car_helpers.os.environ, {'FINGERPRINT': '', 'SKIP_FW_QUERY': '', 'DISABLE_FW_CACHE': ''}), \
|
||||
mock.patch.object(car_helpers.time, 'monotonic', side_effect=lambda: peer.clock), \
|
||||
mock.patch.object(car_helpers.time, 'sleep', side_effect=lambda seconds: setattr(peer, 'clock', peer.clock + seconds)), \
|
||||
mock.patch.object(car_helpers, 'gm_normal_bus0_topology', pedal_normal_bus0_topology if legacy else normal_bus0_topology):
|
||||
if instantiate:
|
||||
result = car_helpers.get_car(peer.recv, peer.send, callbacks.append, False, False)
|
||||
else:
|
||||
result = car_helpers.fingerprint(peer.recv, peer.send, callbacks.append, None)
|
||||
return result, callbacks
|
||||
|
||||
|
||||
def test_actual_sierra_vin_fw_can_and_control_profile_keep_normal_bus0():
|
||||
peer = SierraEcu()
|
||||
result, callbacks = discover(peer)
|
||||
assert callbacks and set(callbacks) == {False}
|
||||
assert peer.sent and all(frame.src == 0 for frame in peer.sent)
|
||||
assert any(frame.dat[0] >> 4 == 3 for frame in peer.sent) # Actual ISO-TP flow control.
|
||||
assert result[0] == SIERRA['expected'] == CAR.CHEVROLET_SILVERADO
|
||||
assert result[3] and all(fw.brand == 'gm' and fw.bus == 0 for fw in result[3])
|
||||
ci, callbacks = discover(SierraEcu(), instantiate=True)
|
||||
direct = CarInterface.get_params(CAR.CHEVROLET_SILVERADO, sierra_fingerprint(), [], False, False, False)
|
||||
assert set(callbacks) == {False}
|
||||
assert ci.CP.safetyConfigs[0].safetyParam == direct.safetyConfigs[0].safetyParam == 0xC171
|
||||
assert ci.CP.flags == direct.flags == 16
|
||||
assert ci.CP.carFingerprint == direct.carFingerprint
|
||||
assert ci.CP.pcmCruise and not ci.CP.openpilotLongitudinalControl
|
||||
assert ci.CP.lateralTuning.to_dict() == direct.lateralTuning.to_dict()
|
||||
|
||||
|
||||
def test_previous_pedal_only_route_reproduces_sierra_mux_and_can1_queries():
|
||||
# Same inert CAN/ECU peer: the old routing path enables the mux and probes
|
||||
# CAN1 even though the GM ECU replies and complete CAN layout are on bus0.
|
||||
peer = SierraEcu()
|
||||
_, callbacks = discover(peer, legacy=True)
|
||||
assert True in callbacks
|
||||
assert any(frame.src == 1 for frame in peer.sent)
|
||||
|
||||
|
||||
def test_sierra_routing_fix_keeps_raw_irq_fault_blocking():
|
||||
from openpilot.cereal import log
|
||||
from openpilot.starpilot.aol.panda_faults import blocking_fault_list, blocking_fault_mask
|
||||
cp = CarInterface.get_params(CAR.CHEVROLET_SILVERADO, sierra_fingerprint(), [], False, False, False)
|
||||
cp.alternativeExperience = 32
|
||||
assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2], cp)
|
||||
assert blocking_fault_mask(8, cp)
|
||||
|
||||
|
||||
def test_latest_bypassed_volt_uses_gm_queries_and_retains_automatic_cc_identity():
|
||||
data = json.loads((Path(__file__).parent / 'testdata/volt_bypassed_discovery.json').read_text())
|
||||
fp = gen_empty_fingerprint()
|
||||
fp.update({int(b): {int(a): n for a, n in messages.items()} for b, messages in data['fingerprint'].items()})
|
||||
assert not pedal_normal_bus0_topology(fp)
|
||||
assert normal_bus0_topology(fp)
|
||||
|
||||
class VoltEcu(RecordedIsoTpEcu):
|
||||
def recv(self, wait_for_one=False):
|
||||
self.clock += .01
|
||||
values, self.frames = self.frames, []
|
||||
return [values + frames(fp)]
|
||||
|
||||
peer = VoltEcu()
|
||||
result, callbacks = discover(peer)
|
||||
assert result[0] == data['expected'] == CAR.CHEVROLET_VOLT_CC
|
||||
assert callbacks and set(callbacks) == {False}
|
||||
assert peer.sent and all(frame.src == 0 for frame in peer.sent)
|
||||
|
||||
|
||||
def test_early_ford_rejection_still_collects_later_gm_frames_in_same_budget():
|
||||
# Actual 2017 Bolt DLCs: 0x204 is four bytes, 0x4f1 is eight. The first
|
||||
# rejects Ford and the second rejects HKG before GM core messages arrive.
|
||||
layout = FINGERPRINTS[CAR.CHEVROLET_BOLT_CC_2017][0]
|
||||
assert layout[0x204] == 4 and layout[0x4F1] == 8
|
||||
early = [CanData(a, bytes(layout[a]), 0) for a in (0x204, 0x4F1)]
|
||||
remaining = frames({0: layout})
|
||||
calls = []
|
||||
clock = [0.]
|
||||
|
||||
def recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
||||
clock[0] += .01
|
||||
calls.append(wait_for_one)
|
||||
return [early if len(calls) == 1 else remaining]
|
||||
assert normal_bus_query_brand(recv, lambda read: hyundai_probe(read, clock=lambda: clock[0]),
|
||||
gm_probe=normal_bus0_topology, clock=lambda: clock[0]) == 'gm'
|
||||
assert 1 < len(calls) <= 13
|
||||
|
||||
|
||||
def test_early_ford_rejection_does_not_add_reads_without_gm_probe():
|
||||
calls = []
|
||||
|
||||
def recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
||||
calls.append(wait_for_one)
|
||||
return [[CanData(0x204, bytes(4), 0), CanData(0x4F1, bytes(8), 0)]]
|
||||
assert normal_bus_query_brand(recv, hyundai_probe, clock=lambda: 0.) is None
|
||||
assert len(calls) == 1
|
||||
@@ -1,168 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.values import CAR, GMFlags
|
||||
from opendbc.car.gm.steering_authority import SteeringAuthority, monitored_profile
|
||||
|
||||
|
||||
def params():
|
||||
# Stock-longitudinal ASCM profile from route5a; no private vehicle data.
|
||||
cp = structs.CarParams.new_message(
|
||||
brand='gm',
|
||||
carFingerprint=str(CAR.CHEVROLET_VOLT_ASCM),
|
||||
flags=16,
|
||||
networkLocation='fwdCamera',
|
||||
pcmCruise=True,
|
||||
openpilotLongitudinalControl=False,
|
||||
alphaLongitudinalAvailable=False,
|
||||
radarUnavailable=True,
|
||||
minSteerSpeed=3.1292800903320312,
|
||||
alternativeExperience=32,
|
||||
)
|
||||
cp.safetyConfigs = [{'safetyModel': 'gm', 'safetyParam': 0x605}]
|
||||
return cp
|
||||
|
||||
|
||||
class TestStockAscmSteeringAuthority(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cp = params()
|
||||
self.monitor = SteeringAuthority(self.cp)
|
||||
|
||||
def step(self, tick, status=1, active=True, pause=None, missing=False, malformed=False):
|
||||
ns = 1_000_000_000 + tick * 10_000_000
|
||||
cs = structs.CarState.new_message(
|
||||
canValid=True,
|
||||
canTimeout=False,
|
||||
vEgo=17.0,
|
||||
standstill=False,
|
||||
gearShifter='low',
|
||||
steeringPressed=pause == 'driver',
|
||||
brakePressed=pause == 'brake',
|
||||
regenBraking=pause == 'paddle',
|
||||
)
|
||||
cs.cruiseState.available = pause != 'main'
|
||||
if pause == 'stopped':
|
||||
cs.standstill = True
|
||||
cs.vEgo = 0.0
|
||||
if pause == 'slow':
|
||||
cs.vEgo = 2.0
|
||||
if pause == 'cancel':
|
||||
cs.buttonEvents = [structs.CarState.ButtonEvent(pressed=True, type='cancel')]
|
||||
cs.steerFaultTemporary = status == 2
|
||||
cs.steerFaultPermanent = status == 3
|
||||
packets = [(0xC9, b'\x00' * 8, 0), (0xBE, b'\x00' * 6, 0), (0xBD, b'\x00' * 7, 0), (0x1E1, b'\x00' * 7, 0)]
|
||||
if tick % 10 == 0 and not missing:
|
||||
packets.append((0x184, b'\x00' if malformed else bytes([status << 3]) + b'\x00' * 7, 0))
|
||||
self.monitor.observe([(ns, packets)])
|
||||
self.monitor.update(cs, ns)
|
||||
self.monitor.command(ns, active)
|
||||
return cs
|
||||
|
||||
def prime(self):
|
||||
for tick in range(40):
|
||||
self.step(tick)
|
||||
self.assertTrue(self.monitor.seen_active)
|
||||
|
||||
def test_exact_recorded_profile_enabled(self):
|
||||
self.assertTrue(self.monitor.enabled)
|
||||
|
||||
def test_both_brake_sources_radar_and_bsm_variants(self):
|
||||
for word in (0x205, 0x605, 0xA05, 0xE05):
|
||||
for bsm in (0, 16):
|
||||
with self.subTest(word=word, bsm=bsm):
|
||||
cp = params()
|
||||
cp.safetyConfigs[0].safetyParam = word
|
||||
cp.radarUnavailable = not bool(word & 0x800)
|
||||
cp.flags = bsm
|
||||
self.assertTrue(monitored_profile(cp))
|
||||
|
||||
def test_neighbor_profiles_and_wrong_owners_refused(self):
|
||||
for field, value in [
|
||||
('brand', 'hyundai'),
|
||||
('carFingerprint', str(CAR.CHEVROLET_BOLT_EUV)),
|
||||
('networkLocation', 'gateway'),
|
||||
('passive', True),
|
||||
('dashcamOnly', True),
|
||||
('notCar', True),
|
||||
('pcmCruise', False),
|
||||
('openpilotLongitudinalControl', True),
|
||||
('flags', int(GMFlags.PEDAL_LONG)),
|
||||
('flags', int(GMFlags.NO_CAMERA)),
|
||||
('radarUnavailable', False),
|
||||
]:
|
||||
with self.subTest(field=field):
|
||||
cp = params()
|
||||
setattr(cp, field, value)
|
||||
self.assertFalse(monitored_profile(cp))
|
||||
for word in (5, 0x1005, 0xC163, 0xC151, 0x4087, 0x605 ^ 0x10, 0x605 ^ 0x100, 0x605 ^ 0x8000):
|
||||
with self.subTest(word=word):
|
||||
cp = params()
|
||||
cp.safetyConfigs[0].safetyParam = word
|
||||
self.assertFalse(monitored_profile(cp))
|
||||
|
||||
def test_unresponsive_eps_latches_and_healthy_feedback_cannot_auto_clear(self):
|
||||
self.prime()
|
||||
for tick in range(40, 81):
|
||||
cs = self.step(tick, status=0)
|
||||
self.assertTrue(self.monitor.latched)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
for tick in range(81, 120):
|
||||
cs = self.step(tick, status=1)
|
||||
self.assertTrue(self.monitor.latched)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
|
||||
def test_driver_pauses_and_positive_recovery(self):
|
||||
for pause in ('brake', 'paddle', 'cancel', 'main', 'driver', 'stopped', 'slow'):
|
||||
with self.subTest(pause=pause):
|
||||
self.monitor = SteeringAuthority(self.cp)
|
||||
self.prime()
|
||||
for tick in range(40, 80):
|
||||
self.step(tick, status=0, active=False, pause=pause)
|
||||
self.assertFalse(self.monitor.latched)
|
||||
for tick in range(80, 120):
|
||||
self.step(tick, status=1)
|
||||
self.assertFalse(self.monitor.latched)
|
||||
self.assertTrue(self.monitor.seen_active)
|
||||
|
||||
def test_stale_eps_feed_remains_a_fault(self):
|
||||
self.prime()
|
||||
for tick in range(40, 90):
|
||||
cs = self.step(tick, missing=True, active=tick < 70)
|
||||
self.assertTrue(self.monitor.latched)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
|
||||
def test_malformed_eps_remains_a_fault(self):
|
||||
self.prime()
|
||||
for tick in range(40, 80):
|
||||
cs = self.step(tick, malformed=True)
|
||||
self.assertTrue(self.monitor.latched)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
|
||||
def test_factory_fault_flags_preserved(self):
|
||||
for status in (2, 3):
|
||||
with self.subTest(status=status):
|
||||
self.monitor = SteeringAuthority(self.cp)
|
||||
self.prime()
|
||||
for tick in range(40, 80):
|
||||
cs = self.step(tick, status=status, active=False)
|
||||
self.assertEqual(cs.steerFaultTemporary, status == 2)
|
||||
self.assertEqual(cs.steerFaultPermanent, status == 3)
|
||||
|
||||
def test_main_cycle_uses_neutral_and_fresh_rearm(self):
|
||||
self.prime()
|
||||
for tick in range(40, 81):
|
||||
self.step(tick, status=0)
|
||||
self.assertTrue(self.monitor.latched)
|
||||
for tick in range(81, 86):
|
||||
self.step(tick, status=0, active=False, pause='main')
|
||||
self.assertTrue(self.monitor.latched)
|
||||
self.step(86, status=0, active=False)
|
||||
self.assertFalse(self.monitor.latched)
|
||||
for tick in range(87, 120):
|
||||
self.step(tick, status=1)
|
||||
self.assertFalse(self.monitor.latched)
|
||||
self.assertTrue(self.monitor.seen_active)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@@ -31,11 +31,6 @@ def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False
|
||||
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}),
|
||||
packer.make_can_msg("ASCMActiveCruiseControlStatus", 2, {"ACCCruiseState": 3, "ACCSpeedSetpoint": 60})]
|
||||
if getattr(ci, 'CP', None) is not None and int(ci.CP.safetyConfigs[0].safetyParam) == 0x1CD:
|
||||
cycle = now // 40_000_000
|
||||
if cycle != getattr(ci, '_fixture_brake_cycle', None):
|
||||
messages.append(packer.make_can_msg("EBCMFrictionBrakeCmd", 2, {"RollingCounter": cycle % 4}))
|
||||
ci._fixture_brake_cycle = cycle
|
||||
ci.update([(now - 1_000_000, messages)])
|
||||
out = ci.update([(now, messages)])
|
||||
return out, messages
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,212 +0,0 @@
|
||||
{
|
||||
"route": "38310abfb83b2669/00000003--d0f66b55b0",
|
||||
"recordedCommit": "39392c37a688f9f8f0a57b0d352d6fad2ca76a95",
|
||||
"source": "First24 original CAN batches, address/DLC/bus only; payloads intentionally omitted.",
|
||||
"packets": [
|
||||
[[532,6,2],[241,6,2],[532,6,0],[241,6,0],[249,8,2],[249,8,0],[460,5,2],[460,5,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[485,8,2],[485,8,0],[190,6,2],[190,6,0],[289,8,2],[289,8,0],[201,8,2],[201,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[211,2,2],[211,2,0],[560,8,2],[560,8,0],[810,8,2],[810,8,0],[209,7,2],[209,7,0],[489,8,2],[489,8,0],[562,8,2],[562,8,0],[241,6,2]],
|
||||
[[241,6,0],[384,4,2],[384,4,0],[800,6,2],[800,6,0],[1033,7,2],[1033,7,0],[1034,7,2],[1034,7,0],[1296,4,2],[1296,4,0],[322,7,2],[322,7,0],[249,8,2],[249,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[761,7,2],[761,7,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[209,7,2],[209,7,0],[451,8,2],[451,8,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0],[241,6,2]],
|
||||
[[455,7,2],[241,6,0],[455,7,0],[715,8,2],[715,8,0],[500,6,2],[500,6,0],[479,3,2],[479,3,0],[789,5,2],[789,5,0],[460,5,2],[460,5,0],[463,3,2],[463,3,0],[869,4,2],[869,4,0],[880,6,2],[880,6,0],[840,5,2],[840,5,0],[481,7,2],[481,7,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[842,5,2],[842,5,0],[249,8,2],[249,8,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[190,6,2],[190,6,0],[209,7,2],[209,7,0]],
|
||||
[[201,8,2],[211,2,2],[201,8,0],[211,2,0],[241,6,2],[241,6,0],[717,5,2],[717,5,0],[489,8,2],[489,8,0],[844,8,2],[844,8,0],[848,4,2],[848,4,0],[497,8,2],[497,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[249,8,2],[249,8,0],[209,7,2],[209,7,0],[455,7,2],[455,7,0]],
|
||||
[[241,6,2],[241,6,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[460,5,2],[460,5,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[208,8,2],[208,8,0],[451,8,2],[451,8,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0],[500,6,2],[500,6,0],[479,3,2],[479,3,0],[528,5,2],[528,5,0],[707,8,2],[707,8,0],[1001,8,2],[1001,8,0],[209,7,2],[209,7,0],[381,8,2],[381,8,0]],
|
||||
[[489,8,2],[489,8,0],[241,6,2],[241,6,0],[532,6,2],[532,6,0],[249,8,2],[249,8,0],[352,5,2],[352,5,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[485,8,2],[485,8,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[211,2,2],[211,2,0],[386,8,2],[386,8,0],[481,7,2],[481,7,0],[560,8,2],[560,8,0],[209,7,2],[209,7,0],[455,7,2],[455,7,0],[562,8,2],[562,8,0]],
|
||||
[[241,6,2],[241,6,0],[789,5,2],[789,5,0],[715,8,2],[715,8,0],[869,4,2],[869,4,0],[880,6,2],[880,6,0],[460,5,2],[460,5,0],[298,8,2],[298,8,0],[249,8,2],[249,8,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[761,7,2],[761,7,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[451,8,2],[451,8,0],[209,7,2],[209,7,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0]],
|
||||
[[241,6,2],[241,6,0],[717,5,2],[717,5,0],[489,8,2],[489,8,0],[500,6,2],[500,6,0],[479,3,2],[479,3,0],[840,5,2],[840,5,0],[977,8,2],[977,8,0],[1011,6,2],[1011,6,0],[1020,8,2],[1020,8,0],[309,8,2],[309,8,0],[388,8,2],[388,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[842,5,2],[842,5,0],[249,8,2],[249,8,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[190,6,2],[190,6,0],[209,7,2],[209,7,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[241,6,2],[241,6,0]],
|
||||
[[455,7,2],[455,7,0],[844,8,2],[844,8,0],[848,4,2],[848,4,0],[460,5,2],[460,5,0],[311,8,2],[311,8,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[481,7,2],[481,7,0],[249,8,2],[249,8,0],[209,7,2],[209,7,0],[608,8,2],[608,8,0]],
|
||||
[[609,6,2],[609,6,0],[489,8,2],[489,8,0],[241,6,2],[241,6,0],[610,6,2],[610,6,0],[611,6,2],[611,6,0],[612,8,2],[612,8,0],[613,8,2],[613,8,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[313,8,2],[313,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[451,8,2],[451,8,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0],[479,3,2],[479,3,0],[500,6,2],[500,6,0],[528,5,2],[528,5,0],[707,8,2],[707,8,0],[1233,8,2],[1233,8,0],[1263,4,2],[1263,4,0],[209,7,2],[209,7,0],[455,7,2],[455,7,0]],
|
||||
[[532,6,2],[532,6,0],[241,6,2],[241,6,0],[789,5,2],[789,5,0],[715,8,2],[715,8,0],[869,4,2],[869,4,0],[249,8,2],[249,8,0],[880,6,2],[880,6,0],[460,5,2],[460,5,0],[810,8,2],[810,8,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[485,8,2],[485,8,0],[190,6,2],[190,6,0],[289,8,2],[289,8,0],[201,8,2],[201,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[211,2,2],[211,2,0],[560,8,2],[560,8,0],[209,7,2],[209,7,0],[489,8,2],[489,8,0],[562,8,2],[562,8,0],[241,6,2],[241,6,0]],
|
||||
[[384,4,2],[384,4,0],[717,5,2],[717,5,0],[800,6,2],[800,6,0],[1033,7,2],[1033,7,0],[1034,7,2],[1034,7,0],[1296,4,2],[1296,4,0],[322,7,2],[322,7,0],[249,8,2],[249,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[761,7,2],[761,7,0],[481,7,2],[481,7,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[451,8,2],[451,8,0],[209,7,2],[209,7,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0],[241,6,2],[241,6,0]],
|
||||
[[455,7,2],[455,7,0],[500,6,2],[500,6,0],[479,3,2],[479,3,0],[840,5,2],[840,5,0],[1017,8,2],[1017,8,0],[460,5,2],[460,5,0],[463,3,2],[463,3,0],[1259,8,2],[1259,8,0],[1265,8,2],[1265,8,0],[1267,1,2],[1267,1,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[842,5,2],[842,5,0],[249,8,2],[249,8,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[190,6,2],[190,6,0],[209,7,2],[209,7,0]],
|
||||
[[201,8,2],[211,2,2],[201,8,0],[211,2,0],[241,6,2],[241,6,0],[489,8,2],[489,8,0],[844,8,2],[844,8,0],[848,4,2],[848,4,0],[497,8,2],[497,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[249,8,2],[249,8,0],[209,7,2],[209,7,0],[455,7,2],[455,7,0]],
|
||||
[[241,6,2],[241,6,0],[789,5,2],[789,5,0],[715,8,2],[715,8,0],[869,4,2],[869,4,0],[880,6,2],[880,6,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[460,5,2],[460,5,0],[481,7,2],[481,7,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[208,8,2],[208,8,0],[451,8,2],[451,8,0],[500,6,2],[500,6,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0],[479,3,2],[479,3,0],[528,5,2],[528,5,0],[707,8,2],[707,8,0],[1001,8,2],[1001,8,0],[209,7,2],[209,7,0],[381,8,2],[381,8,0]],
|
||||
[[489,8,2],[489,8,0],[241,6,2],[241,6,0],[532,6,2],[532,6,0],[717,5,2],[717,5,0],[249,8,2],[249,8,0],[352,5,2],[352,5,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[485,8,2],[485,8,0],[190,6,2],[190,6,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[386,8,2],[386,8,0],[560,8,2],[560,8,0],[209,7,2],[209,7,0],[455,7,2]],
|
||||
[[455,7,0],[562,8,2],[562,8,0],[241,6,2],[241,6,0],[460,5,2],[460,5,0],[298,8,2],[298,8,0],[249,8,2],[249,8,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[761,7,2],[761,7,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[209,7,2],[209,7,0],[451,8,2],[451,8,0]],
|
||||
[[452,8,2],[452,8,0],[453,6,2],[453,6,0],[241,6,2],[241,6,0],[489,8,2],[489,8,0],[500,6,2],[500,6,0],[479,3,2],[479,3,0],[840,5,2],[840,5,0],[977,8,2],[977,8,0],[309,8,2],[309,8,0],[388,8,2],[388,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[842,5,2],[842,5,0],[481,7,2],[481,7,0],[249,8,2],[249,8,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[190,6,2],[190,6,0],[209,7,2],[209,7,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0]],
|
||||
[[241,6,2],[241,6,0],[789,5,2],[789,5,0],[455,7,2],[455,7,0],[715,8,2],[715,8,0],[844,8,2],[844,8,0],[848,4,2],[848,4,0],[869,4,2],[869,4,0],[460,5,2],[460,5,0],[880,6,2],[880,6,0],[311,8,2],[311,8,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[249,8,2],[249,8,0],[209,7,2],[209,7,0],[608,8,2],[608,8,0],[609,6,2],[609,6,0],[610,6,2],[610,6,0],[489,8,2]],
|
||||
[[241,6,2],[489,8,0],[241,6,0],[611,6,2],[611,6,0],[717,5,2],[717,5,0],[612,8,2],[612,8,0],[613,8,2],[613,8,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[313,8,2],[313,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[451,8,2],[451,8,0],[452,8,2],[452,8,0],[453,6,2],[453,6,0],[479,3,2],[479,3,0],[500,6,2],[500,6,0],[528,5,2],[528,5,0],[707,8,2],[707,8,0],[1271,8,2],[1271,8,0],[209,7,2],[209,7,0],[455,7,2],[455,7,0],[532,6,2],[532,6,0],[241,6,2],[241,6,0]],
|
||||
[[249,8,2],[249,8,0],[460,5,2],[460,5,0],[413,8,2],[413,8,0],[481,7,2],[481,7,0],[501,8,2],[501,8,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[190,6,2],[190,6,0],[197,8,2],[197,8,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[560,8,2],[560,8,0],[810,8,2],[810,8,0],[209,7,2],[209,7,0]],
|
||||
[[489,8,2],[489,8,0],[562,8,2],[562,8,0],[241,6,2],[241,6,0],[384,4,2],[384,4,0],[800,6,2],[800,6,0],[1033,7,2],[1033,7,0],[1034,7,2],[1034,7,0],[1296,4,2],[1296,4,0],[322,7,2],[322,7,0],[249,8,2],[249,8,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[761,7,2],[761,7,0],[190,6,2],[190,6,0],[201,8,2],[201,8,0],[211,2,2],[211,2,0],[209,7,2],[209,7,0],[451,8,2],[451,8,0]],
|
||||
[[452,8,2],[452,8,0],[453,6,2],[453,6,0],[241,6,2],[241,6,0],[455,7,2],[455,7,0],[715,8,2],[715,8,0],[500,6,2],[500,6,0],[479,3,2],[479,3,0],[789,5,2],[789,5,0],[869,4,2],[869,4,0],[880,6,2],[880,6,0],[460,5,2],[460,5,0],[463,3,2],[463,3,0],[840,5,2],[840,5,0],[497,8,2],[497,8,0],[485,8,2],[485,8,0],[289,8,2],[289,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[842,5,2],[842,5,0],[249,8,2],[249,8,0],[413,8,2],[413,8,0],[501,8,2],[501,8,0],[190,6,2],[190,6,0],[209,7,2],[209,7,0],[201,8,2],[201,8,0]],
|
||||
[[211,2,2],[211,2,0],[241,6,2],[241,6,0],[717,5,2],[717,5,0],[489,8,2],[489,8,0],[844,8,2],[844,8,0],[848,4,2],[848,4,0],[481,7,2],[481,7,0],[485,8,2],[485,8,0],[193,8,2],[193,8,0],[197,8,2],[197,8,0],[249,8,2],[249,8,0],[209,7,2],[209,7,0],[455,7,2],[455,7,0]]
|
||||
],
|
||||
"fingerprint": {
|
||||
"0": {
|
||||
"452": 8,
|
||||
"453": 6,
|
||||
"241": 6,
|
||||
"455": 7,
|
||||
"789": 5,
|
||||
"715": 8,
|
||||
"500": 6,
|
||||
"479": 3,
|
||||
"869": 4,
|
||||
"880": 6,
|
||||
"840": 5,
|
||||
"1017": 8,
|
||||
"460": 5,
|
||||
"463": 3,
|
||||
"1259": 8,
|
||||
"485": 8,
|
||||
"289": 8,
|
||||
"193": 8,
|
||||
"197": 8,
|
||||
"842": 5,
|
||||
"249": 8,
|
||||
"413": 8,
|
||||
"501": 8,
|
||||
"190": 6,
|
||||
"209": 7,
|
||||
"201": 8,
|
||||
"211": 2,
|
||||
"717": 5,
|
||||
"489": 8,
|
||||
"844": 8,
|
||||
"848": 4,
|
||||
"481": 7,
|
||||
"497": 8,
|
||||
"451": 8,
|
||||
"208": 8,
|
||||
"528": 5,
|
||||
"707": 8,
|
||||
"381": 8,
|
||||
"532": 6,
|
||||
"352": 5,
|
||||
"386": 8,
|
||||
"560": 8,
|
||||
"562": 8,
|
||||
"298": 8,
|
||||
"761": 7,
|
||||
"309": 8,
|
||||
"977": 8,
|
||||
"1001": 8,
|
||||
"388": 8,
|
||||
"311": 8,
|
||||
"608": 8,
|
||||
"609": 6,
|
||||
"610": 6,
|
||||
"611": 6,
|
||||
"612": 8,
|
||||
"613": 8,
|
||||
"313": 8,
|
||||
"810": 8,
|
||||
"384": 4,
|
||||
"800": 6,
|
||||
"1033": 7,
|
||||
"1034": 7,
|
||||
"1296": 4,
|
||||
"322": 7,
|
||||
"1011": 6,
|
||||
"1020": 8,
|
||||
"1217": 8,
|
||||
"1261": 7,
|
||||
"1221": 5,
|
||||
"1233": 8,
|
||||
"1263": 4,
|
||||
"1265": 8,
|
||||
"1267": 1,
|
||||
"1271": 8,
|
||||
"565": 5,
|
||||
"288": 5,
|
||||
"304": 3,
|
||||
"320": 4,
|
||||
"1249": 8,
|
||||
"1300": 8,
|
||||
"328": 1,
|
||||
"1280": 4,
|
||||
"563": 5,
|
||||
"1930": 7
|
||||
},
|
||||
"1": {},
|
||||
"2": {
|
||||
"452": 8,
|
||||
"453": 6,
|
||||
"241": 6,
|
||||
"455": 7,
|
||||
"789": 5,
|
||||
"715": 8,
|
||||
"500": 6,
|
||||
"479": 3,
|
||||
"869": 4,
|
||||
"880": 6,
|
||||
"840": 5,
|
||||
"1017": 8,
|
||||
"460": 5,
|
||||
"463": 3,
|
||||
"1259": 8,
|
||||
"485": 8,
|
||||
"289": 8,
|
||||
"193": 8,
|
||||
"197": 8,
|
||||
"842": 5,
|
||||
"249": 8,
|
||||
"413": 8,
|
||||
"501": 8,
|
||||
"190": 6,
|
||||
"209": 7,
|
||||
"201": 8,
|
||||
"211": 2,
|
||||
"717": 5,
|
||||
"489": 8,
|
||||
"844": 8,
|
||||
"848": 4,
|
||||
"481": 7,
|
||||
"497": 8,
|
||||
"451": 8,
|
||||
"208": 8,
|
||||
"528": 5,
|
||||
"707": 8,
|
||||
"381": 8,
|
||||
"532": 6,
|
||||
"352": 5,
|
||||
"386": 8,
|
||||
"560": 8,
|
||||
"562": 8,
|
||||
"298": 8,
|
||||
"761": 7,
|
||||
"309": 8,
|
||||
"977": 8,
|
||||
"1001": 8,
|
||||
"388": 8,
|
||||
"311": 8,
|
||||
"608": 8,
|
||||
"609": 6,
|
||||
"610": 6,
|
||||
"611": 6,
|
||||
"612": 8,
|
||||
"613": 8,
|
||||
"313": 8,
|
||||
"810": 8,
|
||||
"384": 4,
|
||||
"800": 6,
|
||||
"1033": 7,
|
||||
"1034": 7,
|
||||
"1296": 4,
|
||||
"322": 7,
|
||||
"1011": 6,
|
||||
"1020": 8,
|
||||
"1217": 8,
|
||||
"1261": 7,
|
||||
"1221": 5,
|
||||
"1233": 8,
|
||||
"1263": 4,
|
||||
"1265": 8,
|
||||
"1267": 1,
|
||||
"1271": 8,
|
||||
"565": 5,
|
||||
"288": 5,
|
||||
"304": 3,
|
||||
"320": 4,
|
||||
"1249": 8,
|
||||
"1300": 8,
|
||||
"328": 1,
|
||||
"1280": 4,
|
||||
"563": 5,
|
||||
"1930": 7
|
||||
},
|
||||
"3": {},
|
||||
"4": {},
|
||||
"5": {},
|
||||
"6": {},
|
||||
"7": {}
|
||||
},
|
||||
"expected": "CHEVROLET_SILVERADO"
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"route": "fe40163e0981f4d1/00000001--faea507251",
|
||||
"recordedCommit": "0fcc393380b6bd22bfcd53c6fc92bc4dddca723f",
|
||||
"expected": "CHEVROLET_VOLT_CC",
|
||||
"fingerprint": {
|
||||
"0": {
|
||||
"485": 8,
|
||||
"193": 8,
|
||||
"197": 8,
|
||||
"209": 7,
|
||||
"489": 8,
|
||||
"844": 8,
|
||||
"241": 6,
|
||||
"211": 2,
|
||||
"481": 7,
|
||||
"189": 7,
|
||||
"479": 3,
|
||||
"501": 8,
|
||||
"201": 8,
|
||||
"493": 8,
|
||||
"451": 8,
|
||||
"388": 8,
|
||||
"452": 8,
|
||||
"453": 6,
|
||||
"500": 6,
|
||||
"707": 8,
|
||||
"977": 8,
|
||||
"562": 8,
|
||||
"1001": 8,
|
||||
"386": 8,
|
||||
"840": 5,
|
||||
"842": 5,
|
||||
"560": 8,
|
||||
"497": 8,
|
||||
"532": 6,
|
||||
"566": 5,
|
||||
"761": 7,
|
||||
"528": 4,
|
||||
"352": 5,
|
||||
"298": 8,
|
||||
"309": 8,
|
||||
"311": 8,
|
||||
"313": 8,
|
||||
"381": 6,
|
||||
"608": 8,
|
||||
"609": 6,
|
||||
"610": 6,
|
||||
"611": 6,
|
||||
"612": 8,
|
||||
"613": 8,
|
||||
"810": 8,
|
||||
"1017": 8,
|
||||
"1020": 8,
|
||||
"1217": 8,
|
||||
"304": 1,
|
||||
"1233": 8,
|
||||
"1265": 8,
|
||||
"1267": 1,
|
||||
"320": 3,
|
||||
"1221": 5,
|
||||
"1249": 8,
|
||||
"1300": 8,
|
||||
"328": 1,
|
||||
"563": 5,
|
||||
"308": 4,
|
||||
"1922": 7,
|
||||
"565": 5,
|
||||
"1280": 4
|
||||
},
|
||||
"1": {},
|
||||
"2": {
|
||||
"485": 8,
|
||||
"193": 8,
|
||||
"197": 8,
|
||||
"209": 7,
|
||||
"489": 8,
|
||||
"844": 8,
|
||||
"241": 6,
|
||||
"211": 2,
|
||||
"481": 7,
|
||||
"189": 7,
|
||||
"479": 3,
|
||||
"501": 8,
|
||||
"201": 8,
|
||||
"493": 8,
|
||||
"451": 8,
|
||||
"388": 8,
|
||||
"452": 8,
|
||||
"453": 6,
|
||||
"500": 6,
|
||||
"707": 8,
|
||||
"977": 8,
|
||||
"562": 8,
|
||||
"1001": 8,
|
||||
"386": 8,
|
||||
"840": 5,
|
||||
"842": 5,
|
||||
"560": 8,
|
||||
"497": 8,
|
||||
"532": 6,
|
||||
"566": 5,
|
||||
"761": 7,
|
||||
"528": 4,
|
||||
"352": 5,
|
||||
"298": 8,
|
||||
"309": 8,
|
||||
"311": 8,
|
||||
"313": 8,
|
||||
"381": 6,
|
||||
"608": 8,
|
||||
"609": 6,
|
||||
"610": 6,
|
||||
"611": 6,
|
||||
"612": 8,
|
||||
"613": 8,
|
||||
"810": 8,
|
||||
"1017": 8,
|
||||
"1020": 8,
|
||||
"1217": 8,
|
||||
"304": 1,
|
||||
"1233": 8,
|
||||
"1265": 8,
|
||||
"1267": 1,
|
||||
"320": 3,
|
||||
"1221": 5,
|
||||
"1249": 8,
|
||||
"1300": 8,
|
||||
"328": 1,
|
||||
"563": 5,
|
||||
"308": 4,
|
||||
"1922": 7,
|
||||
"565": 5,
|
||||
"1280": 4
|
||||
},
|
||||
"3": {},
|
||||
"4": {},
|
||||
"5": {},
|
||||
"6": {},
|
||||
"7": {}
|
||||
}
|
||||
}
|
||||
@@ -359,21 +359,6 @@ def is_volt_ascm_longitudinal(cp: CarParams) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def is_volt_ascm_stock(cp: CarParams) -> bool:
|
||||
"""Exact non-pedal ASCM Volt steering owner with factory longitudinal control."""
|
||||
try:
|
||||
configs = cp.safetyConfigs
|
||||
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_VOLT_ASCM and
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.pcmCruise and
|
||||
not cp.openpilotLongitudinalControl and not cp.passive and not cp.dashcamOnly and
|
||||
not cp.notCar and control_flags(cp) == 0 and len(configs) == 1 and
|
||||
configs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
int(configs[0].safetyParam) in (0x205, 0x605, 0xA05, 0xE05) and
|
||||
bool(int(configs[0].safetyParam) & int(GMSafetyFlags.ASCM_RADAR)) is not cp.radarUnavailable)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def is_volt_camera_removed(cp: CarParams, *, longitudinal=None) -> bool:
|
||||
"""The exact manual Volt with PT state sources and no factory camera."""
|
||||
try:
|
||||
|
||||
@@ -81,13 +81,7 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
|
||||
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())
|
||||
|
||||
def can_fingerprint(can_recv, *, vin, camera_cruise_messages):
|
||||
assert callable(can_recv) and vin == initial.carVin
|
||||
assert camera_cruise_messages == set()
|
||||
return None, fingerprint
|
||||
|
||||
discovery.setattr(car_helpers, "can_fingerprint", can_fingerprint)
|
||||
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:
|
||||
|
||||
@@ -333,11 +333,8 @@ def test_actual_card_active_observation_requires_fresh_selfdrive_state(monkeypat
|
||||
host.car_gps_publisher = SimpleNamespace(update=lambda *args: None)
|
||||
host.observe_ioniq6_long_authority = lambda *args: None
|
||||
host.update_vehicle_state_context = lambda *args: None
|
||||
|
||||
def update_cruise_speed(_state, _pending, *, approved=None):
|
||||
assert approved is None
|
||||
host.update_cruise_speed = update_cruise_speed
|
||||
host.v_cruise_helper = SimpleNamespace(slc_consumed_button=None, toyota=None)
|
||||
host.update_cruise_speed = lambda *args: None
|
||||
host.v_cruise_helper = SimpleNamespace(slc_consumed_button=None)
|
||||
host.startup_panda_configured = lambda: True
|
||||
host.can_sock = object()
|
||||
host.pm = None
|
||||
|
||||
@@ -27,10 +27,9 @@ def config_index(cp):
|
||||
return owner
|
||||
raw = int(cp.safetyConfigs[owner].safetyParam)
|
||||
if cp.openpilotLongitudinalControl and not cp.pcmCruise:
|
||||
word = 0x2015 if cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else 0x15
|
||||
if raw == word and cp.alternativeExperience == 0:
|
||||
if raw == 0x15 and cp.alternativeExperience == 0:
|
||||
return 1
|
||||
if cp.carFingerprint in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and raw == (word | 0x800) 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
|
||||
|
||||
@@ -95,7 +95,7 @@ def qualified(cp, *, marked_only=False):
|
||||
return cp.safetyConfigs[config_index(cp)].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER))
|
||||
|
||||
|
||||
LONG_AOL_WORDS = frozenset((0x0815, 0x0895, 0x2815, 0x2895))
|
||||
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}
|
||||
|
||||
@@ -114,10 +114,6 @@ def qualified_long(cp, *, marked_only=False):
|
||||
if word not in (0x11, 0x91):
|
||||
return False
|
||||
raw = int(cp.safetyConfigs[config_index(cp)].safetyParam)
|
||||
if cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
||||
if not raw & 0x2000:
|
||||
return False
|
||||
raw &= ~0x2000
|
||||
return (raw == (word | 0x804) and cp.alternativeExperience == 32) or (
|
||||
not marked_only and raw == (word | 4) and cp.alternativeExperience == 0
|
||||
)
|
||||
|
||||
@@ -26,7 +26,6 @@ from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_ga
|
||||
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.classic_long_aol import independent_buttons_qualified as qualified_classic_long_buttons
|
||||
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
|
||||
@@ -75,11 +74,6 @@ def angle_torque_reduction_gain(steering_torque, v_ego, lat_active, last_gain):
|
||||
return round(rate_limit(target, last_gain, -0.014, 0.004) / 0.004) * 0.004
|
||||
|
||||
|
||||
def classic_aol_dash_enabled(cp):
|
||||
return bool(cp.carFingerprint != CAR.KIA_RAY_EV and (qualified_classic_stock_aol(cp, marked_only=True) or
|
||||
cp.carFingerprint == CAR.HYUNDAI_ELANTRA_2021 and qualified_classic_long_buttons(cp, marked_only=True)))
|
||||
|
||||
|
||||
def process_hud_alert(enabled, fingerprint, hud_control):
|
||||
sys_warning = (hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw))
|
||||
|
||||
@@ -159,7 +153,7 @@ 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 classic_aol_dash_enabled(CP) 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
|
||||
@@ -187,7 +181,7 @@ class CarController(CarControllerBase):
|
||||
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 classic_aol_dash_enabled(self.CP):
|
||||
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)
|
||||
|
||||
@@ -11,14 +11,6 @@ _REQUIRED = (HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CCNC | HyundaiF
|
||||
_IGNORED_EV_GEAR_FLAGS = HyundaiFlags.CANFD_ALT_GEARS | HyundaiFlags.CANFD_ALT_GEARS_2
|
||||
|
||||
|
||||
def metadata_qualified(cp):
|
||||
"""Common transport fields; each caller still checks its own LONG/stock mode."""
|
||||
return (cp.carFingerprint in STOCK_SAFETY_PARAMS and cp.brand == "hyundai" and
|
||||
not (cp.notCar or cp.passive or cp.dashcamOnly) and
|
||||
int(cp.flags) & ~int(_IGNORED_EV_GEAR_FLAGS) == int(_REQUIRED) and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hyundaiCanfd)
|
||||
|
||||
|
||||
def qualified(cp):
|
||||
return (cp.carFingerprint in STOCK_SAFETY_PARAMS and cp.brand == "hyundai" and
|
||||
not (cp.notCar or cp.passive or cp.dashcamOnly or cp.openpilotLongitudinalControl) and cp.pcmCruise and
|
||||
|
||||
@@ -6,7 +6,7 @@ from copy import deepcopy
|
||||
import time
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiSafetyFlags
|
||||
from opendbc.car.hyundai.canfd_owner import config_index
|
||||
from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
|
||||
|
||||
@@ -34,13 +34,6 @@ def stock_copy(cp):
|
||||
stock = reader.as_builder()
|
||||
stock.openpilotLongitudinalControl = False
|
||||
stock.pcmCruise = True
|
||||
if cp.openpilotLongitudinalControl and stock.carFingerprint in (
|
||||
CAR.KIA_CARNIVAL_4TH_GEN, CAR.KIA_CARNIVAL_2025,
|
||||
CAR.KIA_CARNIVAL_HEV_4TH_GEN, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN):
|
||||
for config in stock.safetyConfigs:
|
||||
config.safetyParam &= ~0x2000
|
||||
if stock.carFingerprint in (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN) and stock.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
|
||||
config.safetyParam |= 0x2000
|
||||
for config in stock.safetyConfigs:
|
||||
if stock.carFingerprint == CAR.GENESIS_GV60_EV_1ST_GEN and config.safetyParam in (0x9015, 0x9095):
|
||||
config.safetyParam &= ~0x9000
|
||||
@@ -195,8 +188,7 @@ class HyundaiECUStartup:
|
||||
expected = deepcopy(cast(PreparedParams, self.prepared_cp))
|
||||
configs = expected.get('safetyConfigs', [])
|
||||
owner = config_index(ci.CP)
|
||||
prepared_word = 0x2015 if gv70_eligible(ci.CP) else 0x15
|
||||
if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] != prepared_word or expected['alternativeExperience'] != 0:
|
||||
if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0:
|
||||
raise RuntimeError('Captured ADRV prepared LONG profile cannot be finalized')
|
||||
configs[owner]['safetyParam'] |= 0x0800
|
||||
expected['alternativeExperience'] = 32
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
"""Exact first-generation EV6 startup compatibility wrapper."""
|
||||
from copy import deepcopy
|
||||
import time
|
||||
|
||||
from opendbc.car import make_tester_present_msg
|
||||
from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome
|
||||
from opendbc.car.hyundai.canfd_owner import config_index
|
||||
from opendbc.car.hyundai.captured_adrv_startup import CapturedADRVStartup, first_generation
|
||||
from opendbc.car.hyundai.ev6_template import EV6Template
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
@@ -21,87 +15,3 @@ def required(cp):
|
||||
class EV6Startup(CapturedADRVStartup):
|
||||
def __init__(self, cp, callbacks):
|
||||
super().__init__(cp, callbacks, label='EV6', template_type=EV6Template)
|
||||
|
||||
def prepare_when_controls_ready(self, *, admission):
|
||||
"""Keep factory ADRV output until the inactive control producer is ready."""
|
||||
if self.outcome is not None or self.closed:
|
||||
raise RuntimeError('EV6 startup preparation is single-use')
|
||||
# The existing captured sender is qualified on physical A-CAN zero.
|
||||
if self.template_bus != 0:
|
||||
return self.prepare(admission=admission)
|
||||
if admission() is not True:
|
||||
return self.prepare(admission=lambda: False)
|
||||
self.template = self._capture()
|
||||
if self.template is None:
|
||||
return HyundaiECUStartup.prepare(self, admission=lambda: False)
|
||||
self.admission = admission
|
||||
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
self.outcome = Outcome.PENDING
|
||||
self.prepared_cp = deepcopy(self.cp.to_dict())
|
||||
return self.cp
|
||||
|
||||
def configure(self, ci):
|
||||
super().configure(ci)
|
||||
if self.outcome is Outcome.PENDING:
|
||||
if self.template is None:
|
||||
raise RuntimeError('EV6 pending startup requires its captured template')
|
||||
ci.CC.adrv_template = self.template
|
||||
|
||||
def finalize_aol_configuration(self, ci):
|
||||
if self.outcome is not Outcome.PENDING:
|
||||
return super().finalize_aol_configuration(ci)
|
||||
if self.published or self.closed or ci is not self.ci or not self.ready:
|
||||
raise RuntimeError('EV6 pending AOL finalization requires configured startup')
|
||||
if self.prepared_for(ci.CP):
|
||||
return
|
||||
from opendbc.car.hyundai.ev6_aol import qualified
|
||||
expected = deepcopy(self.prepared_cp)
|
||||
index = config_index(ci.CP)
|
||||
if (not qualified(ci.CP, marked_only=True) or expected is None or index is None or
|
||||
len(expected['safetyConfigs']) != index + 1 or
|
||||
expected['safetyConfigs'][index]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0):
|
||||
raise RuntimeError('EV6 pending startup cannot relabel another safety profile')
|
||||
expected['safetyConfigs'][index]['safetyParam'] |= 0x0800
|
||||
expected['alternativeExperience'] = 32
|
||||
if ci.CP.to_dict() != expected:
|
||||
raise RuntimeError('EV6 pending AOL configuration changed unrelated CarParams')
|
||||
self.prepared_cp = expected
|
||||
|
||||
def sources_current(self, ci, now_ns):
|
||||
if self.outcome is Outcome.PENDING:
|
||||
return self._sources(ci, now_ns, floor=self.floor_ns)
|
||||
return super().sources_current(ci, now_ns)
|
||||
|
||||
def before_control(self, **context):
|
||||
return self.outcome is not Outcome.PENDING and super().before_control(**context)
|
||||
|
||||
def commit_pending(self, ci, *, sources_current, control_current):
|
||||
if (self.outcome is not Outcome.PENDING or self.closed or not self.published or
|
||||
ci is not self.ci or not self.prepared_for(ci.CP) or not sources_current or
|
||||
not control_current or self.admission is None or self.admission() is not True):
|
||||
raise RuntimeError('EV6 takeover requires current diagnostic, source and inactive control admission')
|
||||
template = self._capture()
|
||||
if template is None or not self.sources_current(ci, time.clock_gettime_ns(time.CLOCK_BOOTTIME)) or self.admission() is not True:
|
||||
raise RuntimeError('EV6 takeover lost its current factory capture or source admission')
|
||||
self.template = ci.CC.adrv_template = template
|
||||
try:
|
||||
success = self._disable(self.callbacks[0], self._send, bus=self.bus, addr=self.address,
|
||||
com_cont_req=self.communication_control_request())
|
||||
if not success or not self.disable_attempted:
|
||||
raise RuntimeError('EV6 pending ECU disable was not sent successfully')
|
||||
self.outcome = Outcome.SENT_UNCONFIRMED
|
||||
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
self.keeper = self.keeper_factory(
|
||||
lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock)
|
||||
self.keeper.start()
|
||||
except BaseException:
|
||||
if self.keeper is not None:
|
||||
self.keeper.stop()
|
||||
try:
|
||||
restored = not (self.session_attempted or self.disable_attempted) or self._restore()
|
||||
except Exception:
|
||||
restored = False
|
||||
self.outcome = Outcome.ABORT_UNCERTAIN
|
||||
if not restored:
|
||||
raise RuntimeError('EV6 pending takeover failed; stock restoration unverified') from None
|
||||
raise
|
||||
|
||||
@@ -5,7 +5,7 @@ from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader
|
||||
import numpy as np
|
||||
from opendbc.car import DT_CTRL, structs
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified, metadata_qualified
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified
|
||||
|
||||
class CarControllerParams:
|
||||
ACCEL_MIN = -3.5
|
||||
@@ -314,8 +314,6 @@ def qualified(cp):
|
||||
cp.alternativeExperience not in (0, 32) or
|
||||
not cp.alphaLongitudinalAvailable or len(cp.safetyConfigs) != 1 or cp.safetyConfigs[0].safetyParam != LONG_PARAM):
|
||||
return False
|
||||
if type(cp) is _DynamicStructBuilder and cp.schema == structs.CarParams.schema:
|
||||
return metadata_qualified(cp)
|
||||
stock = stock_copy(cp)
|
||||
stock.alternativeExperience = 0
|
||||
return stock_qualified(stock)
|
||||
@@ -339,10 +337,8 @@ def lateral_request_allowed(control, state, angle_fault, *, independent=False):
|
||||
|
||||
The paired exact EV9 AOL profile may request lateral-only control. Its
|
||||
native physical latch and current axis/session proof remain final authority.
|
||||
Ordinary control retains pedal refusal. Native-qualified independent lateral
|
||||
keeps its request through pedals; both retain standstill, gear, fault and CAN-health refusals.
|
||||
Both modes retain pedal, standstill, gear, fault and CAN-health refusals.
|
||||
"""
|
||||
return ((control.enabled or (independent and control.latActive)) and state.canValid and not state.canTimeout and not state.standstill and
|
||||
state.gearShifter == structs.CarState.GearShifter.drive and
|
||||
not (state.steerFaultTemporary or state.steerFaultPermanent or angle_fault) and
|
||||
((independent and control.latActive) or not (state.brakePressed or state.gasPressed)))
|
||||
not (state.brakePressed or state.gasPressed or state.steerFaultTemporary or state.steerFaultPermanent or angle_fault))
|
||||
|
||||
@@ -71,8 +71,7 @@ def qualified(cp):
|
||||
bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) and not int(cp.flags) & ~int(allowed) and
|
||||
bool(cp.flags & HyundaiFlags.CCNC) == bool(factory.config.flags & HyundaiFlags.CCNC) and
|
||||
stock_qualified(stock) and stock.safetyConfigs[0].safetyParam in STOCK_WORDS and
|
||||
cp.safetyConfigs[0].safetyParam == (stock.safetyConfigs[0].safetyParam | 4 |
|
||||
(0x2000 if cp.carFingerprint in (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN) else 0)))
|
||||
cp.safetyConfigs[0].safetyParam == stock.safetyConfigs[0].safetyParam | 4)
|
||||
|
||||
|
||||
class GenericHDA2Startup(RadarHDA2Startup):
|
||||
|
||||
@@ -57,12 +57,9 @@ def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_torque)
|
||||
lkas_msg = "LKAS_ALT" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS"
|
||||
if CP.openpilotLongitudinalControl:
|
||||
ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
|
||||
# Dom uses separate damping values for camera LKAS and ADAS LFA torque commands.
|
||||
# Preserve the explicit Carnival/GV70 LKAS calibration in either longitudinal mode.
|
||||
lkas_values = values
|
||||
if (not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and CP.carFingerprint not in (
|
||||
CAR.KIA_CARNIVAL_4TH_GEN, CAR.KIA_CARNIVAL_2025,
|
||||
CAR.KIA_CARNIVAL_HEV_4TH_GEN, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)):
|
||||
if (CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl and
|
||||
not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING):
|
||||
lkas_values = {**values, "Damping_Gain": 0}
|
||||
ret.append(packer.make_can_msg(lkas_msg, CAN.ACAN, lkas_values))
|
||||
else:
|
||||
|
||||
@@ -7,8 +7,7 @@ from opendbc.car.hyundai.non_scc_aol import NON_SCC_IDS
|
||||
from opendbc.car.hyundai.values import (is_blended, is_blended_alpha, HyundaiFlags, CAR, DBC, HyundaiSafetyFlags, CANFD_ANGLE_MODEL_BITS,
|
||||
CANFD_ANGLE_OBSERVED_ADAS_BIT, HYUNDAI_MRR35_RADAR_DBC, HYUNDAI_MRR30_RADAR_DBC,
|
||||
HYUNDAI_GV70_RADAR_DBC, HYUNDAI_G90_RADAR_DBC, HYUNDAI_MRREVO14F_RADAR_DBC)
|
||||
from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR, MRR35_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, radar_bus, \
|
||||
stinger_2022_radar_available
|
||||
from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR, MRR35_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, radar_bus
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from opendbc.car.disable_ecu import disable_ecu
|
||||
from opendbc.car.hyundai.carcontroller import CarController
|
||||
@@ -34,16 +33,6 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
RadarInterface = RadarInterface
|
||||
|
||||
@staticmethod
|
||||
def requires_lateral_body_dimensions(cp):
|
||||
# The shared platform identifier does not establish a body/camera envelope.
|
||||
return cp.carFingerprint == CAR.HYUNDAI_IONIQ_6
|
||||
|
||||
@staticmethod
|
||||
def get_lateral_body_dimensions(cp):
|
||||
from opendbc.car.hyundai.lane_body import dimensions
|
||||
return dimensions(cp)
|
||||
|
||||
@staticmethod
|
||||
def startup_required(cp):
|
||||
from opendbc.car.hyundai.g90_startup import required
|
||||
@@ -296,8 +285,6 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.radarUnavailable = fingerprint[radar_bus(ret)].get(MRR30_RADAR_START_ADDR) != 32
|
||||
elif DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_MRR35_RADAR_DBC:
|
||||
ret.radarUnavailable = fingerprint[radar_bus(ret)].get(MRR35_RADAR_START_ADDR) != 24
|
||||
elif candidate == CAR.KIA_STINGER_2022:
|
||||
ret.radarUnavailable = not stinger_2022_radar_available(fingerprint)
|
||||
else:
|
||||
ret.radarUnavailable = radar_start not in fingerprint[1] or Bus.radar not in DBC[ret.carFingerprint]
|
||||
from opendbc.car.hyundai.torque_ev_startup import topology_owned as torque_ev_topology
|
||||
@@ -326,11 +313,6 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
if ret.openpilotLongitudinalControl:
|
||||
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value
|
||||
if (ret.flags & HyundaiFlags.CANFD and ret.flags & HyundaiFlags.CANFD_LKA_STEER_MSG and
|
||||
not ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING and candidate in (
|
||||
CAR.KIA_CARNIVAL_4TH_GEN, CAR.KIA_CARNIVAL_2025,
|
||||
CAR.KIA_CARNIVAL_HEV_4TH_GEN, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)):
|
||||
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CANFD_LKAS_DAMPING_100.value
|
||||
if ret.flags & HyundaiFlags.HYBRID:
|
||||
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.HYBRID_GAS.value
|
||||
elif ret.flags & HyundaiFlags.EV:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from capnp.lib.capnp import _DynamicStructBuilder
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
import threading
|
||||
import time
|
||||
|
||||
from opendbc.car import Bus, make_tester_present_msg
|
||||
from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable
|
||||
from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
from opendbc.car.hyundai.ioniq5pe_long_profile import qualified
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified
|
||||
@@ -41,40 +39,12 @@ class Lease:
|
||||
return self.phase is Phase.OWNED
|
||||
|
||||
|
||||
def _plain_configuration(value):
|
||||
pending = [(value, 0)]
|
||||
while pending:
|
||||
container, depth = pending.pop()
|
||||
kind = type(container)
|
||||
if kind is dict:
|
||||
for key in container:
|
||||
if type(key) is not str:
|
||||
return False
|
||||
children = container.values()
|
||||
elif kind is list:
|
||||
children = container
|
||||
else:
|
||||
return False
|
||||
for child in children:
|
||||
kind = type(child)
|
||||
if kind is int or kind is float or kind is bool or kind is str or kind is bytes or child is None:
|
||||
continue
|
||||
if (kind is not dict and kind is not list) or depth >= 15:
|
||||
return False
|
||||
pending.append((child, depth + 1))
|
||||
return True
|
||||
|
||||
|
||||
def _configuration_bytes(cp):
|
||||
return cp.as_reader().as_builder().to_bytes()
|
||||
|
||||
|
||||
class PEPendingStartup:
|
||||
@staticmethod
|
||||
def profile_qualified(cp):
|
||||
return qualified(cp)
|
||||
|
||||
def __init__(self, cp, callbacks: tuple[CanRecvCallable, CanSendCallable], *, stock_cp, clock=time.monotonic,
|
||||
def __init__(self, cp, callbacks, *, stock_cp, clock=time.monotonic,
|
||||
boot_clock=clock_boot_ns,
|
||||
exchange=query_ioniq6_adas, event_factory=threading.Event, thread_factory=threading.Thread):
|
||||
if not self.profile_qualified(cp) or not stock_qualified(stock_cp) or stock_cp.carFingerprint != cp.carFingerprint:
|
||||
@@ -137,27 +107,9 @@ class PEPendingStartup:
|
||||
return self.owner.cp
|
||||
|
||||
def prepared_for(self, cp):
|
||||
if (self.closed or self.fault is not None or self.snapshot is None or
|
||||
self.owner.phase not in (Phase.PENDING, Phase.OWNED, Phase.STOCK)):
|
||||
return False
|
||||
previous = getattr(self, '_prepared_cp_comparison', None)
|
||||
try:
|
||||
if (previous is not None and previous[0] is self.owner.cp and previous[1] is cp and
|
||||
_plain_configuration(self.snapshot) and self.snapshot == previous[2] and
|
||||
_configuration_bytes(self.owner.cp) == previous[3] and _configuration_bytes(cp) == previous[4]):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
matches = self.owner.cp.to_dict() == self.snapshot == cp.to_dict()
|
||||
self._prepared_cp_comparison = None
|
||||
if (matches and type(self.owner.cp) is _DynamicStructBuilder and type(cp) is _DynamicStructBuilder and
|
||||
_plain_configuration(self.snapshot)):
|
||||
try:
|
||||
self._prepared_cp_comparison = (self.owner.cp, cp, deepcopy(self.snapshot),
|
||||
_configuration_bytes(self.owner.cp), _configuration_bytes(cp))
|
||||
except Exception:
|
||||
pass
|
||||
return matches
|
||||
return (not self.closed and self.fault is None and self.snapshot is not None and
|
||||
self.owner.phase in (Phase.PENDING, Phase.OWNED, Phase.STOCK) and
|
||||
self.owner.cp.to_dict() == self.snapshot == cp.to_dict())
|
||||
|
||||
def configure(self, ci):
|
||||
if self.owner.published or not self.prepared_for(ci.CP):
|
||||
|
||||
@@ -25,8 +25,7 @@ def rear_body(status: BlindspotStatus) -> bytes:
|
||||
body = bytearray(REAR_NEUTRAL_BODY)
|
||||
body[3] = int(bool(left or right)) | (left << 6)
|
||||
body[4] = right
|
||||
# The legacy FL_INDICATOR alias overlaps the right sound-warning field.
|
||||
# Mirror lamps have their own indication bits below.
|
||||
body[5] = left << 1
|
||||
body[6] = right << 5
|
||||
body[7] = 0x80 | (max(left, right) << 3)
|
||||
body[16] = left | (right << 2)
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Narrow body-panel bounds for a corroborated US 2023 IONIQ 6.
|
||||
|
||||
These bounds exclude mirrors and accessories. They are used only for sampled
|
||||
road-edge prediction, not vehicle identification or CAN/startup permission.
|
||||
"""
|
||||
|
||||
CAMERA_FW = bytes.fromhex('f100434520204d46432020415420555341204c484420312e303020312e30342039393231312d4b4c30303020323231323133')
|
||||
RADAR_FW = bytes.fromhex('f10043455f5f20524452202d2d2d2d2d202020202020312e303020312e30312039393131302d4b4c303030202020202020202020')
|
||||
|
||||
|
||||
def _us_my23_vin(vin):
|
||||
if not isinstance(vin, str) or len(vin) != 17 or not vin.startswith('KMH') or vin[9] != 'P' or not 'A' <= vin[6] <= 'Z':
|
||||
return False
|
||||
values = {str(i): i for i in range(10)}
|
||||
values.update({c: i for i, c in enumerate('ABCDEFGH', 1)})
|
||||
values.update({c: i for i, c in enumerate('JKLMN', 1)})
|
||||
values.update({c: i for i, c in enumerate('STUVWXYZ', 2)})
|
||||
values.update(P=7, R=9)
|
||||
if any(c not in values for c in vin):
|
||||
return False
|
||||
weights = (8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2)
|
||||
remainder = sum(values[c] * w for c, w in zip(vin, weights, strict=True)) % 11
|
||||
return vin[8] == ('X' if remainder == 10 else str(remainder))
|
||||
|
||||
|
||||
def dimensions(cp):
|
||||
"""Read body metadata without changing CarParams or startup eligibility."""
|
||||
if (getattr(cp, 'brand', None) != 'hyundai' or getattr(cp, 'carFingerprint', None) != 'HYUNDAI_IONIQ_6'
|
||||
or getattr(cp, 'fuzzyFingerprint', True) is not False or not _us_my23_vin(getattr(cp, 'carVin', None))):
|
||||
return None
|
||||
matched = set()
|
||||
try:
|
||||
for fw in cp.carFw:
|
||||
if fw.logging:
|
||||
continue
|
||||
kind = str(fw.ecu)
|
||||
if kind not in ('fwdCamera', 'fwdRadar'):
|
||||
continue
|
||||
address, version = (0x7c4, CAMERA_FW) if kind == 'fwdCamera' else (0x7d0, RADAR_FW)
|
||||
if (fw.brand != 'hyundai' or fw.address != address or fw.subAddress != 0 or fw.bus not in (0, 1)
|
||||
or bytes(fw.fwVersion) != version):
|
||||
return None
|
||||
matched.add(kind)
|
||||
except (AttributeError, TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
if matched != {'fwdCamera', 'fwdRadar'}:
|
||||
return None
|
||||
# Hyundai US 2023 owner's manual, printed page 2-9: overall W/L/H.
|
||||
# Mirrors are outside this body-panel envelope.
|
||||
return 1.880, 4.855, 1.495
|
||||
@@ -10,7 +10,7 @@ from opendbc.car.hyundai.ecu_continuity import ContinuousECUStartup, StartupKeep
|
||||
from opendbc.car.hyundai.ecu_startup import stock_copy
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.radar_hda1_startup import CARS, RadarHDA1Startup
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
from opendbc.car.hyundai.values import HyundaiFlags
|
||||
|
||||
|
||||
def required(cp):
|
||||
@@ -27,8 +27,7 @@ def qualified(cp):
|
||||
return (required(cp) and cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0 and not cp.pcmCruise and
|
||||
not int(cp.flags) & ~int(allowed) and owner is not None and
|
||||
stock.safetyConfigs[owner].safetyParam in (0x10, 0x12, 0x90, 0x92) and stock_qualified(stock) and
|
||||
cp.safetyConfigs[owner].safetyParam == (stock.safetyConfigs[owner].safetyParam | 4 |
|
||||
(0x2000 if cp.carFingerprint == CAR.KIA_CARNIVAL_4TH_GEN else 0)))
|
||||
cp.safetyConfigs[owner].safetyParam == stock.safetyConfigs[owner].safetyParam | 4)
|
||||
|
||||
|
||||
class RadarHDA2Startup(RadarHDA1Startup):
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import RadarInterfaceBase
|
||||
from opendbc.car.hyundai.values import (CAR, DBC, HyundaiFlags, HYUNDAI_MRR35_RADAR_DBC, HYUNDAI_MRR30_RADAR_DBC,
|
||||
HYUNDAI_GV70_RADAR_DBC, HYUNDAI_G90_RADAR_DBC, is_blended)
|
||||
@@ -23,103 +21,6 @@ MRR35_RADAR_START_ADDR = 0x3A5
|
||||
MRR35_RADAR_MSG_COUNT = 32
|
||||
MRR35_RADAR_CYCLE_NS = 50_000_000 # 20 Hz
|
||||
|
||||
# 2022+ Kia Stinger: 10 object slots x 3 frames (0x238..0x255, 8 bytes, 50 Hz) on bus 1. No radar DBC; decoded by hand.
|
||||
STINGER_2022_RADAR_BUS = 1
|
||||
STINGER_2022_RADAR_START_ADDR = 0x238
|
||||
STINGER_2022_RADAR_SLOT_COUNT = 10
|
||||
STINGER_2022_RADAR_MSG_COUNT = 30
|
||||
STINGER_2022_MAX_TRIPLET_SPAN_NS = 20_000_000
|
||||
|
||||
# Suppress measured near-centre stopped-lead speed undershoot without changing oncoming tracks.
|
||||
STINGER_2022_STOP_UNDERSHOOT_SPEED = -0.7 # m/s
|
||||
STINGER_2022_STOP_UNDERSHOOT_MAX_LATERAL = 2.0 # m
|
||||
|
||||
# Measured radar/wheel-speed bias; taper to zero at standstill.
|
||||
STINGER_2022_EGO_SPEED_OFFSET = 0.14 # m/s
|
||||
STINGER_2022_EGO_SPEED_OFFSET_RAMP = 1.0 # m/s
|
||||
|
||||
|
||||
def stinger_2022_radar(CP) -> bool:
|
||||
return CP.carFingerprint == CAR.KIA_STINGER_2022
|
||||
|
||||
|
||||
def stinger_2022_radar_available(fingerprint) -> bool:
|
||||
return all(fingerprint[STINGER_2022_RADAR_BUS].get(addr) == 8
|
||||
for addr in range(STINGER_2022_RADAR_START_ADDR, STINGER_2022_RADAR_START_ADDR + STINGER_2022_RADAR_MSG_COUNT))
|
||||
|
||||
|
||||
def _field(dat: bytes, start: int, size: int) -> int:
|
||||
return (int.from_bytes(dat, "big") >> (64 - start - size)) & ((1 << size) - 1)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Stinger2022CandidateObject:
|
||||
slot: int
|
||||
object_id: int
|
||||
cycle: int
|
||||
type_raw: int
|
||||
distance: float
|
||||
lateral_raw: float
|
||||
absolute_speed: float
|
||||
|
||||
def relative_speed(self, ego_speed: float) -> float:
|
||||
if not math.isfinite(ego_speed):
|
||||
raise ValueError("ego speed must be finite")
|
||||
radar_ego_speed = ego_speed - STINGER_2022_EGO_SPEED_OFFSET * min(max(ego_speed, 0.0) / STINGER_2022_EGO_SPEED_OFFSET_RAMP, 1.0)
|
||||
return self.settled_absolute_speed() - radar_ego_speed
|
||||
|
||||
def settled_absolute_speed(self) -> float:
|
||||
if STINGER_2022_STOP_UNDERSHOOT_SPEED < self.absolute_speed < 0.0 and \
|
||||
abs(self.lateral_raw) < STINGER_2022_STOP_UNDERSHOOT_MAX_LATERAL:
|
||||
return 0.0
|
||||
return self.absolute_speed
|
||||
|
||||
|
||||
def decode_stinger_2022_triplet(slot: int, payloads: tuple[bytes, ...]) -> Stinger2022CandidateObject | None:
|
||||
if not 0 <= slot < STINGER_2022_RADAR_SLOT_COUNT or len(payloads) != 3 or any(len(dat) != 8 for dat in payloads):
|
||||
return None
|
||||
a, b, c = payloads
|
||||
cycle = _field(a, 44, 2)
|
||||
object_id = _field(a, 56, 7)
|
||||
if cycle != _field(b, 0, 2) or cycle != _field(c, 0, 2):
|
||||
return None
|
||||
if object_id != _field(b, 57, 7) or object_id != _field(c, 43, 7):
|
||||
return None
|
||||
distance_raw = _field(a, 0, 13)
|
||||
if object_id == 0 or distance_raw == 0:
|
||||
return None
|
||||
|
||||
return Stinger2022CandidateObject(
|
||||
slot=slot, object_id=object_id, cycle=cycle, type_raw=_field(a, 13, 3),
|
||||
distance=distance_raw * 0.05, lateral_raw=_field(a, 16, 12) * 0.05 - 102.4,
|
||||
absolute_speed=_field(a, 32, 12) * 0.05 - 100.0,
|
||||
)
|
||||
|
||||
|
||||
class Stinger2022ObjectDecoder:
|
||||
def __init__(self):
|
||||
self.frames: dict[tuple[int, int], tuple[int, bytes]] = {}
|
||||
|
||||
def update(self, timestamp_ns: int, address: int, dat: bytes, bus: int) -> Stinger2022CandidateObject | None:
|
||||
end_addr = STINGER_2022_RADAR_START_ADDR + STINGER_2022_RADAR_MSG_COUNT
|
||||
if bus != STINGER_2022_RADAR_BUS or not STINGER_2022_RADAR_START_ADDR <= address < end_addr or len(dat) != 8:
|
||||
return None
|
||||
slot, part = divmod(address - STINGER_2022_RADAR_START_ADDR, 3)
|
||||
self.frames[slot, part] = (timestamp_ns, dat)
|
||||
if part != 2 or any((slot, i) not in self.frames for i in range(3)):
|
||||
return None
|
||||
triple = [self.frames[slot, i] for i in range(3)]
|
||||
times = [t for t, _ in triple]
|
||||
|
||||
# Consume the complete cycle so repeated final frames cannot reuse old parts.
|
||||
for i in range(3):
|
||||
del self.frames[slot, i]
|
||||
|
||||
if max(times) != timestamp_ns or max(times) - min(times) > STINGER_2022_MAX_TRIPLET_SPAN_NS:
|
||||
return None
|
||||
|
||||
return decode_stinger_2022_triplet(slot, tuple(dat for _, dat in triple))
|
||||
|
||||
|
||||
def blended_track_radar(CP) -> bool:
|
||||
# Exact mixed classic PT / HDA-II steering identity, independently observed
|
||||
@@ -142,9 +43,6 @@ def radar_bus(CP) -> int:
|
||||
|
||||
|
||||
def get_radar_can_parser(CP):
|
||||
if stinger_2022_radar(CP):
|
||||
# Only the ego speed comes from a DBC; the object frames are decoded by hand (Stinger2022ObjectDecoder).
|
||||
return CANParser(DBC[CP.carFingerprint][Bus.pt], [("WHL_SPD11", 50)], 0)
|
||||
if radar_dbc(CP) is None:
|
||||
return None
|
||||
|
||||
@@ -156,15 +54,10 @@ def get_radar_can_parser(CP):
|
||||
20 if mrr35 and addr == start + count - 1 else
|
||||
float("nan") if mrr35 or g90 and addr >= start + RADAR_MSG_COUNT else 20 if gv70 else 50) for addr in range(start, start + count)]
|
||||
mrr30 = radar_dbc(CP) == HYUNDAI_MRR30_RADAR_DBC
|
||||
# MRR35 publishes many unused/unknown fields in each object packet. Decode
|
||||
# the four consumed track fields, retaining parser integrity fields automatically.
|
||||
selected = ({name: ('STATE', 'LONG_DIST', 'LAT_DIST', 'REL_SPEED') for name, _ in messages} if mrr35 else None)
|
||||
return CANParser(radar_dbc(CP), messages, radar_bus(CP) if mrr35 or mrr30 or gv70 else 1, signals=selected)
|
||||
return CANParser(radar_dbc(CP), messages, radar_bus(CP) if mrr35 or mrr30 or gv70 else 1)
|
||||
|
||||
|
||||
def radar_message_range(CP) -> tuple[int, int]:
|
||||
if stinger_2022_radar(CP):
|
||||
return STINGER_2022_RADAR_START_ADDR, STINGER_2022_RADAR_MSG_COUNT
|
||||
if radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC:
|
||||
return RADAR_START_ADDR, G90_RADAR_MSG_COUNT
|
||||
if radar_dbc(CP) in (HYUNDAI_MRR30_RADAR_DBC, HYUNDAI_GV70_RADAR_DBC):
|
||||
@@ -186,12 +79,7 @@ class RadarInterface(RadarInterfaceBase):
|
||||
self.mrr35 = self.start_addr == MRR35_RADAR_START_ADDR
|
||||
self.gv70 = radar_dbc(CP) == HYUNDAI_GV70_RADAR_DBC
|
||||
self.mrr30 = self.start_addr == MRR30_RADAR_START_ADDR and not self.gv70
|
||||
self.stinger_2022 = stinger_2022_radar(CP)
|
||||
self.last_mrr35_trigger_ns = 0
|
||||
if self.stinger_2022:
|
||||
self.stinger_2022_decoder = Stinger2022ObjectDecoder()
|
||||
self.stinger_2022_objects: dict[int, Stinger2022CandidateObject] = {}
|
||||
self.v_ego = 0.0
|
||||
self.trigger_msg = self.start_addr + (RADAR_MSG_COUNT if self.g90 else self.msg_count) - 1
|
||||
|
||||
self.radar_off_can = CP.radarUnavailable
|
||||
@@ -210,8 +98,6 @@ class RadarInterface(RadarInterfaceBase):
|
||||
|
||||
vls = self.rcp.update(can_strings)
|
||||
self.updated_messages.update(vls)
|
||||
if self.stinger_2022:
|
||||
self._update_stinger_2022_tracks(can_strings)
|
||||
|
||||
if self.trigger_msg not in self.updated_messages:
|
||||
return None
|
||||
@@ -223,60 +109,11 @@ class RadarInterface(RadarInterfaceBase):
|
||||
return None
|
||||
self.last_mrr35_trigger_ns = trigger_ns
|
||||
|
||||
rr = self._update_stinger_2022() if self.stinger_2022 else self._update(self.updated_messages)
|
||||
rr = self._update(self.updated_messages)
|
||||
self.updated_messages.clear()
|
||||
|
||||
return rr
|
||||
|
||||
def _update_stinger_2022_tracks(self, can_strings):
|
||||
if "WHL_SPD11" in self.rcp.vl and self.rcp.ts_nanos["WHL_SPD11"]["WHL_SPD_FL"] > 0:
|
||||
whl = self.rcp.vl["WHL_SPD11"]
|
||||
wheel_speed = 0.25 * (whl["WHL_SPD_FL"] + whl["WHL_SPD_FR"] + whl["WHL_SPD_RL"] + whl["WHL_SPD_RR"])
|
||||
self.v_ego = wheel_speed * CV.KPH_TO_MS * self.CP.wheelSpeedFactor
|
||||
|
||||
end_addr = self.start_addr + self.msg_count
|
||||
for timestamp_ns, frames in can_strings:
|
||||
for address, dat, src in frames:
|
||||
if src != STINGER_2022_RADAR_BUS or not (self.start_addr <= address < end_addr) or len(dat) != 8:
|
||||
continue
|
||||
|
||||
self.updated_messages.add(address)
|
||||
obj = self.stinger_2022_decoder.update(timestamp_ns, address, bytes(dat), src)
|
||||
if obj is not None:
|
||||
self.stinger_2022_objects[obj.object_id] = obj
|
||||
|
||||
def _update_stinger_2022(self):
|
||||
ret = structs.RadarData()
|
||||
if not self.rcp.can_valid or self.rcp.ts_nanos["WHL_SPD11"]["WHL_SPD_FL"] == 0:
|
||||
# objects decoded in an error cycle must not be published in the next one
|
||||
self.stinger_2022_objects.clear()
|
||||
ret.errors.canError = True
|
||||
return ret
|
||||
|
||||
current_ids = set()
|
||||
for object_id, obj in self.stinger_2022_objects.items():
|
||||
current_ids.add(object_id)
|
||||
pt = self.pts.get(object_id)
|
||||
if pt is None:
|
||||
pt = structs.RadarData.RadarPoint()
|
||||
pt.trackId = self.track_id
|
||||
self.track_id += 1
|
||||
self.pts[object_id] = pt
|
||||
|
||||
pt.dRel = obj.distance
|
||||
pt.yRel = -obj.lateral_raw # in car frame's y axis, left is positive
|
||||
pt.vRel = obj.relative_speed(self.v_ego)
|
||||
|
||||
# tracks not seen this cycle are dropped
|
||||
for oid in list(self.pts.keys()):
|
||||
if oid not in current_ids:
|
||||
del self.pts[oid]
|
||||
|
||||
self.stinger_2022_objects.clear()
|
||||
|
||||
ret.points = list(self.pts.values())
|
||||
return ret
|
||||
|
||||
def _update(self, updated_messages):
|
||||
ret = structs.RadarData()
|
||||
if self.rcp is None:
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Dom 823a5d5 camera/ADAS damping contract, checked at the CAN wire boundary."""
|
||||
from itertools import product
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus, create_steering_messages
|
||||
from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags
|
||||
|
||||
|
||||
TORQUE_CARS = tuple(car for car in CAR if car.config.flags & HyundaiFlags.CANFD
|
||||
and not car.config.flags & HyundaiFlags.CANFD_ANGLE_STEERING)
|
||||
LKAS_DAMPING_100 = frozenset((CAR.KIA_CARNIVAL_4TH_GEN, CAR.KIA_CARNIVAL_2025,
|
||||
CAR.KIA_CARNIVAL_HEV_4TH_GEN, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car', TORQUE_CARS)
|
||||
@pytest.mark.parametrize('topology', ('lfa', 'lkas', 'lkas_alt'))
|
||||
def test_dom_torque_wire_is_independent_of_alpha_long(car, topology):
|
||||
cp = structs.CarParams()
|
||||
cp.carFingerprint = car
|
||||
cp.flags = int(car.config.flags & ~(HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT))
|
||||
if topology != 'lfa':
|
||||
cp.flags |= int(HyundaiFlags.CANFD_LKA_STEER_MSG)
|
||||
if topology == 'lkas_alt':
|
||||
cp.flags |= int(HyundaiFlags.CANFD_LKA_STEER_MSG_ALT)
|
||||
can = CanBus(cp)
|
||||
for enabled, active, torque in product((False, True), (False, True), (-137, 0, 139)):
|
||||
by_mode = []
|
||||
for longitudinal in (False, True):
|
||||
cp.openpilotLongitudinalControl = longitudinal
|
||||
packets = create_steering_messages(CANPacker(DBC[car][Bus.pt]), cp, can, enabled, active, torque)
|
||||
steering_address = {'lfa': 0x12A, 'lkas': 0x50, 'lkas_alt': 0x110}[topology]
|
||||
expected = [(steering_address, can.ECAN if topology == 'lfa' else can.ACAN)]
|
||||
if longitudinal and topology != 'lfa':
|
||||
expected.insert(0, (0x12A, can.ECAN))
|
||||
assert [(addr, bus) for addr, _, bus in packets] == expected
|
||||
for address, data, _ in packets:
|
||||
lfa = address == 0x12A
|
||||
damping = 100 if lfa or car in LKAS_DAMPING_100 else 0
|
||||
# Independent frozen DBC bit positions; no parser/helper-derived oracle.
|
||||
assert data[13 if lfa else 8] == damping
|
||||
bits = int.from_bytes(data, 'little')
|
||||
assert (bits >> 41 & 0x7FF) - 1024 == torque
|
||||
assert (bits >> 52 & 3) == int(active)
|
||||
assert (bits >> 38 & 7) == (2 if enabled else 1)
|
||||
by_mode.append(packets[-1])
|
||||
assert by_mode[0] == by_mode[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('car', tuple(car for car in CAR if car.config.flags & HyundaiFlags.CANFD_ANGLE_STEERING))
|
||||
def test_torque_helper_keeps_existing_angle_fallback_damping(car):
|
||||
cp = structs.CarParams()
|
||||
cp.carFingerprint = car
|
||||
cp.flags = int(car.config.flags | HyundaiFlags.CANFD_LKA_STEER_MSG)
|
||||
for longitudinal in (False, True):
|
||||
cp.openpilotLongitudinalControl = longitudinal
|
||||
packets = create_steering_messages(CANPacker(DBC[car][Bus.pt]), cp, CanBus(cp), True, True, 0)
|
||||
assert packets[-1][1][8] == 100
|
||||
@@ -45,6 +45,6 @@ def test_constructor_scope_preserves_saved_button_mappings_and_release(lda, lkas
|
||||
before = cp.to_dict()
|
||||
settings = AolSettings(True, 0., lkas, main, (0, 0, 0), (0, 0, 0))
|
||||
owner = ClassicLongCardIntent(cp, settings)
|
||||
assert owner.independent_cruise_buttons == (lda and not release)
|
||||
assert owner.independent_cruise_buttons == (lda and not release and lkas == 9 and main == 0)
|
||||
assert cp.to_dict() == before and owner.settings == settings
|
||||
assert not independent_buttons_qualified(cp)
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car import Bus, gen_empty_fingerprint, structs
|
||||
from opendbc.car.hyundai.classic_long_aol import aol_word
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.values import CAR, DBC
|
||||
from opendbc.safety.tests import test_hyundai_classic_long_aol as native_fixture
|
||||
from opendbc.safety.tests.libsafety.libsafety_py import make_CANPacket
|
||||
|
||||
|
||||
def controller(*, marked=True, experience=32):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[0][0x391] = 8
|
||||
fp[0][0x38d] = 8
|
||||
fp[2][0x485] = 4
|
||||
cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_2021, fp, [], True, False, False)
|
||||
if marked:
|
||||
cp.safetyConfigs[0].safetyParam = aol_word(cp)
|
||||
cp.alternativeExperience = experience
|
||||
assert cp.flags == 16843136 # Recorded Elantra alpha-long profile, including FCA and LDA.
|
||||
ci = CarInterface(cp)
|
||||
ci.update([])
|
||||
return ci
|
||||
|
||||
|
||||
def step(ci, cc, parser=None):
|
||||
stamp = 10_000_000_000 + ci.CC.frame * 10_000_000
|
||||
actuators, packets = ci.CC.update(cc.as_reader(), ci.CS, stamp)
|
||||
if parser is not None:
|
||||
parser.update((stamp, packets))
|
||||
return actuators, packets
|
||||
|
||||
|
||||
def parser():
|
||||
return CANParser(DBC[CAR.HYUNDAI_ELANTRA_2021][Bus.pt], [('LKAS11', 0), ('LFAHDA_MFC', 0)], 0)
|
||||
|
||||
|
||||
def test_elantra_long_lateral_only_dash_matches_native_permission_after_cancel():
|
||||
case = native_fixture.TestHyundaiClassicLongAol(methodName='runTest')
|
||||
case.setUp()
|
||||
try:
|
||||
ci, can = controller(), parser()
|
||||
assert ci.CP.safetyConfigs[0].safetyParam == 0xC04
|
||||
case.reset(0xC04)
|
||||
for _ in range(6):
|
||||
case.feed(0xC04)
|
||||
case.feed(0xC04, button=2)
|
||||
case.feed(0xC04)
|
||||
assert case.request(3) == 3
|
||||
case.feed(0xC04, button=4)
|
||||
permission = case.request(3)
|
||||
assert permission == 1 and not case.safety.get_controls_allowed()
|
||||
cc = structs.CarControl(enabled=False, latActive=bool(permission & 1))
|
||||
actuators, packets = step(ci, cc, can)
|
||||
assert can.vl['LKAS11']['CF_Lkas_FcwOpt_USM'] == 2
|
||||
assert can.vl['LFAHDA_MFC']['LFA_Icon_State'] == 2
|
||||
assert actuators.torque == 0 and actuators.accel == 0
|
||||
for address, data, bus in packets:
|
||||
assert case.safety.safety_tx_hook(make_CANPacket(address, bus, data))
|
||||
finally:
|
||||
case.tearDown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('enabled,lateral', [(False, False), (True, False), (True, True), (False, True)])
|
||||
@pytest.mark.parametrize('alert', [structs.CarControl.HUDControl.VisualAlert.none, structs.CarControl.HUDControl.VisualAlert.steerRequired,
|
||||
structs.CarControl.HUDControl.VisualAlert.fcw])
|
||||
def test_only_icon_fields_and_lkas_checksum_change(enabled, lateral, alert):
|
||||
current, original = controller(), controller()
|
||||
original.CC.classic_dash_icons = None
|
||||
new_can, old_can = parser(), parser()
|
||||
cc = structs.CarControl(enabled=enabled, latActive=lateral, hudControl={'visualAlert': alert, 'leadVisible': True})
|
||||
for _ in range(10):
|
||||
new_actuators, new_packets = step(current, cc, new_can)
|
||||
with patch('opendbc.car.hyundai.carcontroller.classic_aol_dash_enabled', return_value=False):
|
||||
old_actuators, old_packets = step(original, cc, old_can)
|
||||
assert new_actuators.to_dict() == old_actuators.to_dict()
|
||||
assert len(new_packets) == len(old_packets)
|
||||
for new, old in zip(new_packets, old_packets, strict=True):
|
||||
assert new[0] == old[0] and new[2] == old[2]
|
||||
if new[0] not in (0x340, 0x485):
|
||||
assert new == old # Includes all SCC/FCA acceleration, lead, and AEB status packets.
|
||||
for message, excluded in [('LKAS11', {'CF_Lkas_FcwOpt_USM', 'CF_Lkas_Chksum'}), ('LFAHDA_MFC', {'LFA_Icon_State'})]:
|
||||
assert {k: v for k, v in new_can.vl[message].items() if k not in excluded} == {
|
||||
k: v for k, v in old_can.vl[message].items() if k not in excluded}
|
||||
|
||||
|
||||
def test_disengagement_blink_and_inactive_icon_restore():
|
||||
ci, can = controller(), parser()
|
||||
for frame in range(101):
|
||||
step(ci, structs.CarControl(latActive=frame == 0), can)
|
||||
icon = 2 if frame == 0 else 3 if frame < 100 else 0
|
||||
assert can.vl['LKAS11']['CF_Lkas_FcwOpt_USM'] == (icon or 1)
|
||||
if frame % 5 == 0:
|
||||
assert can.vl['LFAHDA_MFC']['LFA_Icon_State'] == icon
|
||||
|
||||
|
||||
@pytest.mark.parametrize('marked,experience', [(False, 0), (False, 32), (True, 0), (True, 33)])
|
||||
def test_unqualified_elantra_does_not_gain_lateral_only_icon_override(marked, experience):
|
||||
ci, can = controller(marked=marked, experience=experience), parser()
|
||||
step(ci, structs.CarControl(latActive=True), can)
|
||||
assert can.vl['LKAS11']['CF_Lkas_FcwOpt_USM'] == 1
|
||||
assert can.vl['LFAHDA_MFC']['LFA_Icon_State'] == 0
|
||||
@@ -1,208 +0,0 @@
|
||||
"""Keep factory SCC until an inactive producer is current; never bypass native RX."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import Bus, CanData, structs
|
||||
from opendbc.car.hyundai.ecu_startup import Outcome
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.radar_interface import RadarInterface
|
||||
from opendbc.car.hyundai.tests.test_torque_ev_startup import Keeper, physical_frames
|
||||
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
||||
from opendbc.car.hyundai.ioniq6_handoff import TimestampedCanPacket
|
||||
from openpilot.starpilot.tests.test_ev6_startup import params
|
||||
from opendbc.can.packer import CANPacker
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", (
|
||||
"healthy_suppressed", "healthy_positive", "stale_control", "active_control", "missing_source",
|
||||
"native_mode", "capture_lost", "cp_changed", "cruise_fault", "stock_engaged",
|
||||
"disable_negative", "disable_malformed", "restore_failure", "publisher_failure"))
|
||||
def test_actual_card_defers_ev6_until_inactive_producer_and_then_native_rx(scenario):
|
||||
native = libsafety_py.libsafety
|
||||
assert native.set_safety_hooks(int(structs.CarParams.SafetyModel.elm327), 1) == 0
|
||||
native.init_tests()
|
||||
now = [100_000_000_000]
|
||||
requests, emitted = [], []
|
||||
muted = [False]
|
||||
|
||||
def send(frames):
|
||||
for address, data, bus in frames:
|
||||
assert native.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
emitted.extend(frames)
|
||||
if scenario == "publisher_failure" and any(data[:4] == b"\x03\x28\x83\x01" for _, data, _ in frames):
|
||||
muted[0] = True
|
||||
raise OSError("diagnostic publisher failed after possible ECU delivery")
|
||||
|
||||
def query(sender, receiver, bus, addresses, commands, responses):
|
||||
command = commands[0]
|
||||
requests.append((now[0], command))
|
||||
|
||||
class Query:
|
||||
def get_data(self, timeout):
|
||||
sender([(0x730, bytes([len(command)]) + command + bytes(7 - len(command)), bus)])
|
||||
if command == b"\x28\x83\x01":
|
||||
muted[0] = True
|
||||
if scenario in ("disable_negative", "restore_failure"):
|
||||
return {(0x730, None): b"\x7f\x28\x22"}
|
||||
if scenario == "disable_malformed":
|
||||
return {(0x730, None): b"\x68"}
|
||||
return {(0x730, None): b"\x68\x03"} if scenario == "healthy_positive" else {}
|
||||
if command == b"\x28\x00\x01":
|
||||
if scenario == "restore_failure":
|
||||
return {}
|
||||
muted[0] = False
|
||||
return {(0x730, None): b""}
|
||||
return Query()
|
||||
|
||||
def sleep(seconds):
|
||||
now[0] += round(seconds * 1e9)
|
||||
|
||||
def update_sm(host, command, stale=False):
|
||||
message = messaging.new_message("carControl", valid=True, logMonoTime=now[0] - (151_000_000 if stale else 0))
|
||||
message.carControl = command
|
||||
panda = messaging.new_message("pandaStates", 1, valid=True, logMonoTime=now[0])
|
||||
state = panda.pandaStates[0]
|
||||
state.safetyModel, state.safetyParam = native.get_current_safety_mode(), native.get_current_safety_param()
|
||||
state.alternativeExperience = host.CP.alternativeExperience
|
||||
state.ignitionLine = True
|
||||
state.safetyRxChecksInvalid = native.safety_test_toyota_rx_summary_invalid()
|
||||
host.sm.update_msgs(now[0] / 1e9, [message.as_reader(), panda.as_reader()])
|
||||
|
||||
factory = CarInterface.startup_owner
|
||||
|
||||
def startup_factory(*args, **kwargs):
|
||||
instance = factory(*args, **kwargs)
|
||||
instance.clock, instance.keeper_factory, instance._query = lambda: now[0] / 1e9, Keeper, query
|
||||
return instance
|
||||
|
||||
with OpenpilotPrefix(), patch.object(CarInterface, "startup_owner", side_effect=startup_factory), \
|
||||
patch.dict(os.environ, {"SIMULATION": "0", "AOL_REPLAY_RUNTIME": "1"}), \
|
||||
patch("time.clock_gettime_ns", side_effect=lambda clock: now[0]), \
|
||||
patch("time.monotonic_ns", side_effect=lambda: now[0]), \
|
||||
patch("time.monotonic", side_effect=lambda: now[0] / 1e9), \
|
||||
patch("time.sleep", side_effect=sleep), \
|
||||
patch("opendbc.car.hyundai.ecu_startup.IsoTpParallelQuery", side_effect=query):
|
||||
saved = Params()
|
||||
for key in ("OpenpilotEnabledToggle", "AlwaysOnLateral", "AlphaLongitudinalEnabled"):
|
||||
saved.put_bool(key, True, block=True)
|
||||
holder = VehicleStartupOwner()
|
||||
counter = [0]
|
||||
|
||||
lose_capture = [False]
|
||||
|
||||
def receive():
|
||||
if lose_capture[0]:
|
||||
return []
|
||||
now[0] += 10_000_000
|
||||
counter[0] += 1
|
||||
data = bytearray(32)
|
||||
data[2] = counter[0] % 256
|
||||
data[4] = 7
|
||||
data[:2] = hkg_can_fd_checksum(0x51, None, data).to_bytes(2, "little")
|
||||
return [TimestampedCanPacket([CanData(0x51, bytes(data), 0)], now[0])]
|
||||
prepared = holder.prepare(params(), CarInterface, (receive, send), requested=True,
|
||||
admission=lambda: True,
|
||||
defer_until_controls=True)
|
||||
startup = holder.owner
|
||||
assert startup is not None
|
||||
startup.clock, startup.keeper_factory, startup._query = lambda: now[0] / 1e9, Keeper, query
|
||||
try:
|
||||
assert startup.outcome is Outcome.PENDING and not requests and not emitted
|
||||
assert not startup.before_control(configured=True, sources_current=True, control_current=True)
|
||||
card = Car(CI=CarInterface(prepared), RI=RadarInterface(prepared), startup_owner=startup)
|
||||
startup.admission = card.startup_diagnostic_admission
|
||||
card.params.put_bool("IsOffroad", False, block=True)
|
||||
original_cp = card.CP.to_dict()
|
||||
packer = CANPacker(card.CI.can_parsers[Bus.pt].dbc.name)
|
||||
# Explicit synthetic delayed producer: stock has not been told to stop.
|
||||
for tick in range(1, 701):
|
||||
now[0] += 10_000_000
|
||||
state = card.CI.update([(now[0], physical_frames(card.CP, packer, tick))])
|
||||
assert state.canValid and not state.canTimeout and not state.accFaulted
|
||||
assert not requests and not muted[0] and startup.keeper is None
|
||||
command = structs.CarControl(enabled=scenario == "active_control", latActive=scenario == "active_control").as_reader()
|
||||
if scenario == "missing_source":
|
||||
parser = card.CI.can_parsers[Bus.pt]
|
||||
parser.message_states[parser.dbc.name_to_msg["TCS"].address].timestamps.clear()
|
||||
if scenario == "cruise_fault":
|
||||
state.accFaulted = True
|
||||
if scenario == "stock_engaged":
|
||||
state.cruiseState.enabled = True
|
||||
if scenario == "capture_lost":
|
||||
lose_capture[0] = True
|
||||
if scenario == "cp_changed":
|
||||
card.CP.steerActuatorDelay += 0.01
|
||||
config = card.CP.safetyConfigs[-1]
|
||||
if scenario == "native_mode":
|
||||
native.set_alternative_experience(card.CP.alternativeExperience)
|
||||
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
|
||||
update_sm(card, command, stale=scenario == "stale_control")
|
||||
if scenario == "capture_lost":
|
||||
with pytest.raises(RuntimeError, match="lost its current factory capture"):
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
assert not requests and not muted[0] and not card.ci_initialized
|
||||
assert startup.outcome is Outcome.PENDING
|
||||
return
|
||||
if scenario in ("disable_negative", "disable_malformed", "restore_failure", "publisher_failure"):
|
||||
with pytest.raises(RuntimeError, match="pending"):
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
assert startup.outcome is Outcome.ABORT_UNCERTAIN and not card.ci_initialized
|
||||
assert card.CP.to_dict() == original_cp and not startup.handed_off
|
||||
assert muted[0] == (scenario == "restore_failure")
|
||||
assert startup.restored == (scenario != "restore_failure")
|
||||
return
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
if not scenario.startswith("healthy"):
|
||||
assert not requests and not muted[0] and not card.ci_initialized
|
||||
assert startup.outcome is Outcome.PENDING
|
||||
if scenario != "cp_changed":
|
||||
assert card.CP.to_dict() == original_cp
|
||||
return
|
||||
assert [command for _, command in requests] == [b"\x10\x03", b"\x28\x83\x01"]
|
||||
assert muted[0] and card.ci_initialized and startup.outcome is Outcome.SENT_UNCONFIRMED
|
||||
assert startup.keeper.started and not startup.handed_off
|
||||
# No SCC while native is diagnostic, mismatched, or awaiting RX health.
|
||||
emitted.clear()
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
assert not emitted and not startup.handed_off
|
||||
native.set_alternative_experience(card.CP.alternativeExperience)
|
||||
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
|
||||
native.init_tests()
|
||||
update_sm(card, command)
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
assert not emitted and not startup.handed_off
|
||||
for tick in range(701, 811):
|
||||
now[0] += 10_000_000
|
||||
frames = physical_frames(card.CP, packer, tick)
|
||||
state = card.CI.update([(now[0], frames)])
|
||||
native.set_timer(tick * 10_000)
|
||||
for address, data, bus in frames:
|
||||
native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
if tick % 100 == 0:
|
||||
native.safety_tick()
|
||||
assert native.safety_config_valid()
|
||||
update_sm(card, command)
|
||||
captured = []
|
||||
|
||||
def publish(frames, valid=True):
|
||||
if valid:
|
||||
send(frames)
|
||||
card.vehicle_startup.sent(frames, valid=valid)
|
||||
captured.extend(frames)
|
||||
|
||||
with patch.object(card, "publish_sendcan", side_effect=publish):
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
assert any(address == 0x1A0 and bus == 1 for address, _, bus in captured)
|
||||
assert startup.handed_off and not startup.keeper.started
|
||||
assert not native.get_controls_allowed() and native.aol_get_permission_mask() == 0
|
||||
assert card.CP.to_dict() == original_cp
|
||||
finally:
|
||||
holder.close()
|
||||
@@ -52,23 +52,7 @@ class TestEV9CombinedProfile(unittest.TestCase):
|
||||
cc = structs.CarControl(latActive=True)
|
||||
self.assertFalse(lateral_request_allowed(cc, cs, False))
|
||||
self.assertTrue(lateral_request_allowed(cc, cs, False, independent=qualified(cp, marked_only=True)))
|
||||
ordinary = structs.CarControl(enabled=True, latActive=True)
|
||||
for field in ('gasPressed', 'brakePressed'):
|
||||
setattr(cs, field, True)
|
||||
self.assertTrue(temporary_restriction(cp, cs), field) # Intent retention is not output authority.
|
||||
self.assertTrue(lateral_request_allowed(cc, cs, False, independent=qualified(cp, marked_only=True)), field)
|
||||
self.assertFalse(lateral_request_allowed(ordinary, cs, False), field)
|
||||
self.assertFalse(lateral_request_allowed(structs.CarControl(), cs, False, independent=True), field)
|
||||
self.assertFalse(lateral_request_allowed(structs.CarControl(enabled=True), cs, False, independent=True), field)
|
||||
for refusal, value in (('canValid', False), ('canTimeout', True), ('steerFaultTemporary', True),
|
||||
('steerFaultPermanent', True), ('standstill', True), ('gearShifter', 'reverse')):
|
||||
before = getattr(cs, refusal)
|
||||
setattr(cs, refusal, value)
|
||||
self.assertFalse(lateral_request_allowed(cc, cs, False, independent=True), (field, refusal))
|
||||
setattr(cs, refusal, before)
|
||||
self.assertFalse(lateral_request_allowed(cc, cs, True, independent=True), field)
|
||||
setattr(cs, field, False)
|
||||
for field in ('steerFaultTemporary', 'standstill'):
|
||||
for field in ('gasPressed', 'brakePressed', 'steerFaultTemporary', 'standstill'):
|
||||
setattr(cs, field, True)
|
||||
self.assertTrue(temporary_restriction(cp, cs), field)
|
||||
self.assertFalse(lateral_request_allowed(cc, cs, False, independent=True), field)
|
||||
|
||||
@@ -197,105 +197,3 @@ class TestEV9LongController(unittest.TestCase):
|
||||
self.assertFalse(ci.CS.out.canTimeout)
|
||||
self.assertTrue(ci.CS.out.cruiseState.available)
|
||||
self.assertTrue(ci.CS.out.cruiseState.enabled)
|
||||
|
||||
|
||||
class TestEV9FreshMetadataQualification(unittest.TestCase):
|
||||
@staticmethod
|
||||
def original(cp):
|
||||
from opendbc.car.hyundai.ev9_longitudinal import LONG_PARAM, stock_copy
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified
|
||||
if (cp.carFingerprint != CAR.KIA_EV9 or not cp.openpilotLongitudinalControl or cp.pcmCruise or
|
||||
cp.alternativeExperience not in (0, 32) or not cp.alphaLongitudinalAvailable or
|
||||
len(cp.safetyConfigs) != 1 or cp.safetyConfigs[0].safetyParam != LONG_PARAM):
|
||||
return False
|
||||
stock = stock_copy(cp)
|
||||
stock.alternativeExperience = 0
|
||||
return stock_qualified(stock)
|
||||
|
||||
def base(self):
|
||||
return candidate(params(candidate=CAR.KIA_EV9), enabled=True, is_release=False)
|
||||
|
||||
def check(self, cp):
|
||||
def result(fn):
|
||||
try:
|
||||
return ('value', fn(cp))
|
||||
except Exception as error:
|
||||
return ('error', type(error), str(error))
|
||||
self.assertEqual(result(qualified), result(self.original))
|
||||
|
||||
def test_current_builder_fields_and_every_safety_word(self):
|
||||
cp = self.base()
|
||||
for experience in (0, 32):
|
||||
cp.alternativeExperience = experience
|
||||
for word in range(65536):
|
||||
cp.safetyConfigs[0].safetyParam = word
|
||||
self.assertEqual(qualified(cp), self.original(cp), (experience, word))
|
||||
|
||||
def test_each_live_metadata_field_and_neighbor_profile(self):
|
||||
cp = self.base()
|
||||
changes = {
|
||||
'brand': ('', 'gm', 'hyundai'),
|
||||
'carFingerprint': ('', CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV9),
|
||||
'flags': (0, *(int(cp.flags) ^ (1 << bit) for bit in range(32))),
|
||||
'alternativeExperience': (0, 32, 64, 96, 128),
|
||||
'openpilotLongitudinalControl': (False, True), 'pcmCruise': (False, True),
|
||||
'alphaLongitudinalAvailable': (False, True), 'notCar': (False, True),
|
||||
'passive': (False, True), 'dashcamOnly': (False, True),
|
||||
}
|
||||
for field, values in changes.items():
|
||||
for value in values:
|
||||
changed = cp.copy()
|
||||
setattr(changed, field, value)
|
||||
with self.subTest(field=field, value=value):
|
||||
self.check(changed)
|
||||
for count in (0, 2):
|
||||
changed = cp.copy()
|
||||
changed.init('safetyConfigs', count)
|
||||
self.check(changed)
|
||||
for model in ('silent', 'noOutput', 'toyota', 'gm', 'hyundai', 'hyundaiCanfd'):
|
||||
changed = cp.copy()
|
||||
changed.safetyConfigs[0].safetyModel = model
|
||||
self.check(changed)
|
||||
|
||||
def test_ignored_gear_metadata_and_live_mutations(self):
|
||||
from opendbc.car.hyundai.values import HyundaiFlags
|
||||
cp = self.base()
|
||||
for bits in (0, int(HyundaiFlags.CANFD_ALT_GEARS), int(HyundaiFlags.CANFD_ALT_GEARS_2),
|
||||
int(HyundaiFlags.CANFD_ALT_GEARS | HyundaiFlags.CANFD_ALT_GEARS_2)):
|
||||
cp.flags = int(self.base().flags) | bits
|
||||
self.assertTrue(qualified(cp))
|
||||
self.check(cp)
|
||||
cp.safetyConfigs[0].safetyParam ^= 1
|
||||
self.assertFalse(qualified(cp))
|
||||
self.check(cp)
|
||||
cp.safetyConfigs[0].safetyParam ^= 1
|
||||
self.assertTrue(qualified(cp))
|
||||
|
||||
def test_valid_builder_avoids_copy_and_reader_keeps_copy_contract(self):
|
||||
from unittest.mock import patch
|
||||
from opendbc.car.hyundai import ev9_longitudinal
|
||||
cp = self.base()
|
||||
with patch.object(ev9_longitudinal, 'stock_copy', side_effect=AssertionError('unexpected whole CP copy')):
|
||||
self.assertTrue(qualified(cp))
|
||||
with structs.CarParams.from_bytes(cp.to_bytes()) as reader:
|
||||
with patch.object(ev9_longitudinal, 'stock_copy', wraps=ev9_longitudinal.stock_copy) as copied:
|
||||
self.assertTrue(qualified(reader))
|
||||
self.assertEqual(copied.call_count, 1)
|
||||
|
||||
def test_custom_owner_keeps_original_access_and_copy_behavior(self):
|
||||
cp = self.base()
|
||||
|
||||
class Proxy:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def __getattr__(self, field):
|
||||
self.calls.append(field)
|
||||
return getattr(cp, field)
|
||||
|
||||
def to_dict(self):
|
||||
self.calls.append('to_dict')
|
||||
return cp.to_dict()
|
||||
current, original = Proxy(), Proxy()
|
||||
self.assertEqual(qualified(current), self.original(original))
|
||||
self.assertEqual(current.calls, original.calls)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
@@ -10,7 +9,7 @@ from opendbc.car import Bus, CanData, structs
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.ev9_longitudinal import candidate, qualified
|
||||
from opendbc.car.hyundai.ev9_pending_startup import EV9PendingStartup as PEPendingStartup
|
||||
from opendbc.car.hyundai.ioniq5pe_startup import Lease, Phase
|
||||
from opendbc.car.hyundai.ioniq5pe_startup import Phase
|
||||
from opendbc.car.hyundai.ioniq5pe_ready import PEReadySource
|
||||
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
||||
from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params as stock_params
|
||||
@@ -591,7 +590,7 @@ class TestEV9PendingIdentity(unittest.TestCase):
|
||||
stock = stock_params()
|
||||
long = pe_candidate(stock, enabled=True, is_release=False)
|
||||
with self.assertRaises(ValueError):
|
||||
OriginalPEPendingStartup(long, (lambda wait_for_one=False, **kw: [], lambda frames: None), stock_cp=params())
|
||||
OriginalPEPendingStartup(long, (lambda **kw: [], lambda frames: None), stock_cp=params())
|
||||
|
||||
|
||||
class TestEV9UnpreparedController(unittest.TestCase):
|
||||
@@ -608,120 +607,3 @@ class TestEV9UnpreparedController(unittest.TestCase):
|
||||
control.actuators.accel = 1.
|
||||
_, frames = ci.apply(control.as_reader(), 130_000_000)
|
||||
self.assertEqual(frames, [])
|
||||
|
||||
|
||||
class TestPreparedConfigurationComparison(unittest.TestCase):
|
||||
@staticmethod
|
||||
def context(car=CAR.KIA_EV9):
|
||||
cp = stock_params(candidate=car)
|
||||
owner = PEPendingStartup.__new__(PEPendingStartup)
|
||||
owner.owner = Lease(cp.copy())
|
||||
owner.owner.phase = Phase.OWNED
|
||||
owner.closed = False
|
||||
owner.fault = None
|
||||
owner.snapshot = deepcopy(cp.to_dict())
|
||||
assert owner.prepared_for(cp)
|
||||
return owner, cp
|
||||
|
||||
@staticmethod
|
||||
def leaf_paths(value, path=()):
|
||||
if type(value) is dict:
|
||||
for key, child in value.items():
|
||||
yield from TestPreparedConfigurationComparison.leaf_paths(child, path + (key,))
|
||||
elif type(value) is list:
|
||||
for key, child in enumerate(value):
|
||||
yield from TestPreparedConfigurationComparison.leaf_paths(child, path + (key,))
|
||||
else:
|
||||
yield path, value
|
||||
|
||||
@staticmethod
|
||||
def assign(cp, path, value):
|
||||
for key in path[:-1]:
|
||||
cp = getattr(cp, key) if type(key) is str else cp[key]
|
||||
if type(path[-1]) is str:
|
||||
setattr(cp, path[-1], value)
|
||||
else:
|
||||
cp[path[-1]] = value
|
||||
|
||||
def test_every_legal_configuration_mutation_remains_freshly_rejected(self):
|
||||
checked = 0
|
||||
for car in (CAR.KIA_EV9, CAR.HYUNDAI_IONIQ_5_PE):
|
||||
for path, value in self.leaf_paths(stock_params(candidate=car).to_dict()):
|
||||
if type(value) is bool:
|
||||
changed = not value
|
||||
elif type(value) is int:
|
||||
changed = value ^ 1
|
||||
elif type(value) is float:
|
||||
changed = value + .125
|
||||
elif type(value) is str:
|
||||
changed = value + 'changed'
|
||||
elif type(value) is bytes:
|
||||
changed = value + b'changed'
|
||||
else:
|
||||
continue
|
||||
for target in ('owner', 'caller', 'snapshot'):
|
||||
owner, cp = self.context(car)
|
||||
if target == 'snapshot':
|
||||
container = owner.snapshot
|
||||
for key in path[:-1]:
|
||||
container = container[key]
|
||||
container[path[-1]] = changed
|
||||
else:
|
||||
try:
|
||||
self.assign(owner.owner.cp if target == 'owner' else cp, path, changed)
|
||||
except (ValueError, TypeError, AttributeError, RuntimeError):
|
||||
# Invalid enum/union assignments never become a configuration.
|
||||
continue
|
||||
self.assertFalse(owner.prepared_for(cp), (car, path, target))
|
||||
checked += 1
|
||||
self.assertGreater(checked, 300)
|
||||
|
||||
def test_owner_fault_and_phase_are_never_cached(self):
|
||||
for phase in Phase:
|
||||
owner, cp = self.context()
|
||||
owner.owner.phase = phase
|
||||
self.assertEqual(owner.prepared_for(cp), phase in (Phase.PENDING, Phase.OWNED, Phase.STOCK))
|
||||
for name, value in (('closed', True), ('fault', 'current fault'), ('snapshot', None)):
|
||||
owner, cp = self.context()
|
||||
setattr(owner, name, value)
|
||||
self.assertFalse(owner.prepared_for(cp))
|
||||
|
||||
def test_equivalent_replacements_arena_changes_and_reader_fallback(self):
|
||||
owner, cp = self.context()
|
||||
original = cp.carFingerprint
|
||||
cp.carFingerprint += 'temporary'
|
||||
cp.carFingerprint = original
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
cp = cp.copy()
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
owner.owner.cp = owner.owner.cp.copy()
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
self.assertTrue(owner.prepared_for(cp.as_reader()))
|
||||
cp.mass = owner.owner.cp.mass = 0.
|
||||
owner.snapshot['mass'] = -0.
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
cp.mass = owner.owner.cp.mass = float('nan')
|
||||
owner.snapshot['mass'] = float('nan')
|
||||
self.assertFalse(owner.prepared_for(cp))
|
||||
|
||||
def test_custom_snapshot_callbacks_keep_original_semantics(self):
|
||||
trace = []
|
||||
|
||||
class Meta(type):
|
||||
def __eq__(self, other):
|
||||
raise AssertionError('Metadata validation must not invoke metaclass equality')
|
||||
|
||||
def __hash__(self):
|
||||
raise AssertionError('Metadata validation must not invoke metaclass hashing')
|
||||
|
||||
class EqualValue(metaclass=Meta):
|
||||
def __eq__(self, other):
|
||||
trace.append(other)
|
||||
return True
|
||||
|
||||
owner, cp = self.context()
|
||||
mass = owner.snapshot['mass']
|
||||
owner.snapshot['mass'] = EqualValue()
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
self.assertEqual(trace, [mass, mass])
|
||||
self.assertIsNone(owner._prepared_cp_comparison)
|
||||
|
||||
@@ -45,9 +45,7 @@ FUEL_CASES = tuple((identity, hybrid) for identity in sorted(CARS) for hybrid in
|
||||
@pytest.mark.parametrize('alt', [False, True])
|
||||
def test_existing_generic_hda2_long_requires_prepublication_owner(identity, hybrid, alt):
|
||||
cp = params(identity, hybrid, alt=alt)
|
||||
gas = 1 if identity.config.flags & HyundaiFlags.EV else 2 if hybrid or identity == CAR.KIA_CARNIVAL_HEV_4TH_GEN else 0
|
||||
calibration = 0x2000 if identity in (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN) else 0
|
||||
assert cp.openpilotLongitudinalControl and cp.safetyConfigs[0].safetyParam == (0x14 | gas | (128 if alt else 0) | calibration)
|
||||
assert cp.openpilotLongitudinalControl and cp.safetyConfigs[0].safetyParam in (0x14, 0x15, 0x16, 0x94, 0x95, 0x96)
|
||||
sent = []
|
||||
owner = CarInterface.startup_owner(cp, (list, sent.extend), requested=True)
|
||||
assert not sent
|
||||
|
||||
@@ -28,25 +28,14 @@ def long_params():
|
||||
|
||||
|
||||
class TestIoniq6BlindspotStatus(unittest.TestCase):
|
||||
def test_all_sixteen_reference_outputs_without_overlapping_sound_alias(self):
|
||||
def test_all_sixteen_frozen_executable_outputs_and_integrity(self):
|
||||
for case in CASES:
|
||||
with self.subTest(case=case):
|
||||
left = 1 + int(case["leftBlinker"]) if case["left"] else 0
|
||||
right = 1 + int(case["rightBlinker"]) if case["right"] else 0
|
||||
frames = create_ioniq6_blindspot_status(case["counter"], BlindspotStatus(left, right))
|
||||
self.assertEqual([(addr, bus) for addr, _, bus in frames], [(0x1BA, 1), (0x1E5, 1)])
|
||||
expected_rear = bytearray.fromhex(case["rear"])
|
||||
expected_rear[5] = 0 # Retired FL_INDICATOR alias overlaps BCW_RtSndWrngSta.
|
||||
expected_rear[:2] = hkg_can_fd_checksum(0x1BA, None, expected_rear).to_bytes(2, "little")
|
||||
self.assertEqual([payload.hex() for _, payload, _ in frames], [expected_rear.hex(), case["front"]])
|
||||
rear = frames[0][1]
|
||||
sound_word = int.from_bytes(rear, "little")
|
||||
self.assertEqual((sound_word >> 34) & 0x3, 0)
|
||||
self.assertEqual((sound_word >> 36) & 0x3ff, 0)
|
||||
self.assertEqual(rear[16] & 3, left)
|
||||
self.assertEqual((rear[16] >> 2) & 0x3f, right)
|
||||
self.assertEqual((rear[3] >> 6) & 3, left)
|
||||
self.assertEqual(rear[4] & 3, right)
|
||||
self.assertEqual([payload.hex() for _, payload, _ in frames], [case["rear"], case["front"]])
|
||||
for addr, payload, _ in frames:
|
||||
self.assertEqual(int.from_bytes(payload[:2], "little"), hkg_can_fd_checksum(addr, None, bytearray(payload)))
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class TestIoniq6EngagementMessages(unittest.TestCase):
|
||||
stock = cp.as_reader().as_builder()
|
||||
stock.openpilotLongitudinalControl = False
|
||||
packets = create_steering_messages(CANPacker(DBC[cp.carFingerprint][Bus.pt]), stock, CanBus(stock), True, True, 20)
|
||||
self.assertEqual(packets[-1][1][8], 0)
|
||||
self.assertEqual(packets[-1][1][8], 100)
|
||||
angle = cp.as_reader().as_builder()
|
||||
angle.flags |= int(HyundaiFlags.CANFD_ANGLE_STEERING)
|
||||
packets = create_steering_messages(CANPacker(DBC[cp.carFingerprint][Bus.pt]), angle, CanBus(angle), True, True, 20)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
@@ -19,19 +18,6 @@ from opendbc.car.hyundai.values import CAR
|
||||
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
||||
|
||||
|
||||
def refresh_host_parser_receipts(card):
|
||||
from collections import deque
|
||||
from opendbc.can.parser import MessageState
|
||||
|
||||
stamp = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
parsers = {}
|
||||
for bus in (1, 2):
|
||||
source = MessageState(address=bus, name=f"source{bus}", size=8, signals=[],
|
||||
timeout_threshold=100_000_000, timestamps=deque([stamp]))
|
||||
parsers[bus] = SimpleNamespace(message_states={bus: source})
|
||||
card.CI.can_parsers = parsers
|
||||
|
||||
|
||||
class FakeIoniq6Bus:
|
||||
def __init__(self, *, source=True, heartbeat_source=True, heartbeat_stays_after_disable=False,
|
||||
bsm_source=True, bsm_stays_after_disable=False, missing_status=None,
|
||||
@@ -575,9 +561,6 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card.ioniq6_host_warmed = False
|
||||
card.ioniq6_long_lost = False
|
||||
card.ioniq6_keepalive = None
|
||||
card.ioniq6_handoff_source_floor_ns = 1
|
||||
card.CI = SimpleNamespace()
|
||||
refresh_host_parser_receipts(card)
|
||||
invalid = SimpleNamespace(canValid=False)
|
||||
card.observe_ioniq6_long_authority([], invalid)
|
||||
self.assertFalse(card.ioniq6_long_lost)
|
||||
@@ -676,7 +659,6 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card.controls_update(cs, cc)
|
||||
card.CI.apply.assert_not_called()
|
||||
cs.canValid = True
|
||||
refresh_host_parser_receipts(card)
|
||||
card.observe_ioniq6_long_authority([], cs)
|
||||
card.controls_update(cs, cc)
|
||||
card.CI.apply.assert_called_once()
|
||||
@@ -870,8 +852,8 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card_owner.is_metric = True
|
||||
card_owner.experimental_mode = False
|
||||
card_owner.initial_cruise_speed = 40
|
||||
card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_, slc_enabled=False, slc_approved=None: None,
|
||||
initialize_v_cruise=lambda *_, **kwargs: None, slc_consumed_button=None, toyota=None,
|
||||
card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_, slc_enabled=False: None, initialize_v_cruise=lambda *_, **kwargs: None,
|
||||
slc_consumed_button=None,
|
||||
slc_cruise_change=None, v_cruise_kph=60.0, v_cruise_cluster_kph=60.0)
|
||||
card_owner.RI = SimpleNamespace(update=lambda _: None)
|
||||
control = car.CarControl(enabled=True, longActive=True)
|
||||
@@ -905,12 +887,6 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
update=Mock(side_effect=lambda packets: car.CarState(canValid=True, canTimeout=False)),
|
||||
apply=Mock(return_value=(structs.CarControl.Actuators(), [CanData(0x1A0, b'\0' * 8, 1)])),
|
||||
init=Mock())
|
||||
|
||||
def parsed_update(_packets):
|
||||
refresh_host_parser_receipts(card_owner)
|
||||
return car.CarState(canValid=True, canTimeout=False)
|
||||
|
||||
card_owner.CI.update.side_effect = parsed_update
|
||||
card_owner.ioniq6_panda_diagnostic_ready = Mock(return_value=True)
|
||||
card_owner.ioniq6_panda_matches = Mock(side_effect=(False, True, True))
|
||||
packets = iter((
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"""Recorded parser grace must not complete a new I6 ECU ownership warmup."""
|
||||
from collections import deque
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
|
||||
|
||||
def recorded_owner():
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[0][0x3A5] = 24
|
||||
fp[2].update({0x110: 32, 0x362: 32})
|
||||
fp[1].update({0x130: 16, 0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1BA: 24, 0x1E5: 16, 0x36A: 16})
|
||||
stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fp, [], False, False, False)
|
||||
cp = build_ioniq6_hda2_long_candidate(stock, fp)
|
||||
assert cp is not None
|
||||
cp.safetyConfigs[-1].safetyParam |= 0x0800
|
||||
assert cp.flags == 1059329 and cp.safetyConfigs[-1].safetyParam == 0x8895
|
||||
owner = Car.__new__(Car)
|
||||
owner.CP, owner.CI = cp, CarInterface(cp)
|
||||
owner.ioniq6_long_selected = owner.ioniq6_long_prearmed = True
|
||||
owner.ioniq6_long_lost = owner.ioniq6_host_warmed = False
|
||||
owner.ioniq6_keepalive = SimpleNamespace(stop=Mock())
|
||||
return owner
|
||||
|
||||
|
||||
def test_recorded_c8_actual_parser_grace_waits_for_post_handoff_sources():
|
||||
fixture = json.loads((Path(__file__).parent / "testdata/ioniq6_post_handoff_warmup.json").read_text())
|
||||
owner = recorded_owner()
|
||||
floor = fixture["handoffCompletedNs"]
|
||||
owner.ioniq6_handoff_source_floor_ns = floor
|
||||
now = [0]
|
||||
history = []
|
||||
with patch("time.clock_gettime_ns", side_effect=lambda clock: now[0]):
|
||||
for packet in fixture["packets"]:
|
||||
ns = packet["ns"]
|
||||
if fixture["lastCardSourceBeforeTransactionNs"] < ns <= floor:
|
||||
continue # The real UDS transaction consumed this Card cursor interval.
|
||||
now[0] = ns
|
||||
frames = [(a, bytes.fromhex(data), bus) for a, data, bus in packet["frames"]]
|
||||
state = owner.CI.update([(ns, frames)])
|
||||
if ns <= floor:
|
||||
continue
|
||||
parser_valid = state.canValid
|
||||
owner.observe_ioniq6_long_authority([(ns, frames)], state)
|
||||
history.append((ns, parser_valid, owner.ioniq6_host_warmed, owner.ioniq6_long_lost))
|
||||
assert history[0][1] is True, "Recorded parser initially tolerates stale sources"
|
||||
assert history[0][2] is False, "STALE_PARSER_GRACE_MUST_NOT_FINISH_HANDOFF_WARMUP"
|
||||
assert any(not row[1] for row in history), "Real parser grace eventually expires"
|
||||
assert history[-1][1:3] == (True, True)
|
||||
assert not any(row[3] for row in history)
|
||||
owner.ioniq6_keepalive.stop.assert_not_called()
|
||||
# A genuinely invalid input after completed warmup retains the old refusal.
|
||||
invalid = SimpleNamespace(canValid=False)
|
||||
owner.observe_ioniq6_long_authority([], invalid)
|
||||
assert owner.ioniq6_long_lost and not invalid.canValid
|
||||
owner.ioniq6_keepalive.stop.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fault", ("no_floor", "empty_inventory", "pre_handoff", "future", "expired", "bad_counter", "missing"))
|
||||
def test_warmup_proof_rejects_missing_or_invalid_source_receipts(fault):
|
||||
owner = recorded_owner()
|
||||
owner.ioniq6_handoff_source_floor_ns = 1_000_000_000
|
||||
now = 1_100_000_000
|
||||
required = [source for parser in owner.CI.can_parsers.values()
|
||||
for source in parser.message_states.values() if not source.ignore_alive]
|
||||
assert len(required) >= 6
|
||||
for source in required:
|
||||
source.timestamps = deque([now])
|
||||
source = required[-1]
|
||||
if fault == "no_floor":
|
||||
owner.ioniq6_handoff_source_floor_ns = 0
|
||||
elif fault == "empty_inventory":
|
||||
owner.CI.can_parsers = {}
|
||||
elif fault == "pre_handoff":
|
||||
source.timestamps = deque([owner.ioniq6_handoff_source_floor_ns])
|
||||
elif fault == "future":
|
||||
source.timestamps = deque([now + 1])
|
||||
elif fault == "expired":
|
||||
source.timeout_threshold = 10_000_000
|
||||
source.timestamps = deque([now - 10_000_001])
|
||||
elif fault == "bad_counter":
|
||||
source.counter_fail = 5
|
||||
elif fault == "missing":
|
||||
source.timestamps.clear()
|
||||
with patch("time.clock_gettime_ns", return_value=now):
|
||||
owner.observe_ioniq6_long_authority([], SimpleNamespace(canValid=True))
|
||||
assert not owner.ioniq6_host_warmed and not owner.ioniq6_long_lost
|
||||
|
||||
|
||||
@pytest.mark.parametrize("address,bus", ((0x1A0, 1), (0x1BA, 1), (0x1E5, 1), (0x100, 0)))
|
||||
def test_factory_source_return_still_revokes_during_warmup(address, bus):
|
||||
owner = recorded_owner()
|
||||
owner.ioniq6_handoff_source_floor_ns = 1_000_000_000
|
||||
state = SimpleNamespace(canValid=True)
|
||||
owner.observe_ioniq6_long_authority([(1_100_000_000, [(address, bytes(32), bus)])], state)
|
||||
assert owner.ioniq6_long_lost and not state.canValid
|
||||
owner.ioniq6_keepalive.stop.assert_called_once()
|
||||
@@ -1,56 +0,0 @@
|
||||
from copy import deepcopy
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
|
||||
from opendbc.car.hyundai.lane_body import CAMERA_FW, RADAR_FW, dimensions
|
||||
|
||||
|
||||
def identity():
|
||||
# A generated checksum-valid metadata fixture, not a captured vehicle VIN.
|
||||
vin = list('KMHAAAAA0PA000001')
|
||||
values = dict(zip('0123456789ABCDEFGHJKLMNPRSTUVWXYZ', '012345678912345678123457923456789', strict=True))
|
||||
remainder = sum(int(values[c]) * w for c, w in zip(vin, (8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2), strict=True)) % 11
|
||||
vin[8] = 'X' if remainder == 10 else str(remainder)
|
||||
firmware = [SimpleNamespace(ecu=kind, fwVersion=version, address=address, subAddress=0, bus=1, brand='hyundai', logging=False)
|
||||
for kind, version, address in (('fwdCamera', CAMERA_FW, 0x7c4), ('fwdRadar', RADAR_FW, 0x7d0))]
|
||||
return SimpleNamespace(brand='hyundai', carFingerprint='HYUNDAI_IONIQ_6', fuzzyFingerprint=False, carVin=''.join(vin), carFw=firmware)
|
||||
|
||||
|
||||
class TestLaneBody(unittest.TestCase):
|
||||
def test_corroborated_metadata_is_read_only_and_has_explicit_body_height(self):
|
||||
cp = identity()
|
||||
before = deepcopy(cp)
|
||||
self.assertEqual(dimensions(cp), (1.880, 4.855, 1.495))
|
||||
self.assertEqual(cp, before)
|
||||
|
||||
def test_absent_or_conflicting_identity_has_no_positive_bounds(self):
|
||||
for field, value in (('brand', 'other'), ('carFingerprint', 'HYUNDAI_IONIQ_5'), ('fuzzyFingerprint', True),
|
||||
('fuzzyFingerprint', None), ('carVin', ''), ('carVin', '0' * 17), ('carFw', [])):
|
||||
cp = identity()
|
||||
setattr(cp, field, value)
|
||||
self.assertIsNone(dimensions(cp))
|
||||
cp = identity()
|
||||
for index, value in ((9, 'R'), (8, '0'), (6, '1'), (0, 'J')):
|
||||
changed = deepcopy(cp)
|
||||
changed.carVin = cp.carVin[:index] + value + cp.carVin[index + 1:]
|
||||
self.assertIsNone(dimensions(changed))
|
||||
self.assertIsNone(dimensions(object()))
|
||||
|
||||
def test_unrecognized_incomplete_logging_or_conflicting_firmware_is_refused(self):
|
||||
for index in (0, 1):
|
||||
for field, value in (('fwVersion', b'unknown'), ('brand', 'other'), ('address', 0), ('subAddress', 1), ('bus', 4), ('logging', True)):
|
||||
cp = identity()
|
||||
setattr(cp.carFw[index], field, value)
|
||||
self.assertIsNone(dimensions(cp))
|
||||
cp = identity()
|
||||
cp.carFw.pop(index)
|
||||
self.assertIsNone(dimensions(cp))
|
||||
cp = identity()
|
||||
conflict = deepcopy(cp.carFw[0])
|
||||
conflict.fwVersion = b'other'
|
||||
cp.carFw.append(conflict)
|
||||
self.assertIsNone(dimensions(cp))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user