Compare commits

..

5 Commits

Author SHA1 Message Date
Prabhaav Pillai 3b1ce61fb8 fix updater 2026-10-20 01:19:32 -04:00
Prabhaav Pillai 6c93a41942 Test update 2026-10-20 01:14:30 -04:00
Prabhaav Pillai b4154fdc5b Merge commit '0e7503448c2f3fd2678ceeb6ab3799be0f517c65' into test_updates 2026-10-20 01:12:15 -04:00
Prabhaav Pillai ea7de662dc test update 2026-10-20 01:07:24 -04:00
Prabhaav Pillai cc5b618884 update 2026-10-20 01:05:44 -04:00
2150 changed files with 13050 additions and 225060 deletions
-14
View File
@@ -12,12 +12,6 @@ openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl -text
*.woff2 -text
*.wav -text
openpilot/starpilot/galaxy/web/vendor/vue/vue.esm-browser.js -text
*.apk -text
openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 -text
openpilot/starpilot/soloist/pulseaudio/bin/pulseaudio -text
openpilot/starpilot/soloist/pulseaudio/bin/pactl -text
openpilot/starpilot/soloist/bluealsa/bin/bluealsa-aplay -text
openpilot/starpilot/soloist/bluealsa/bin/bluealsad -text
openpilot/selfdrive/car/tests/test_models_segs.txt -text
openpilot/common/hardware/comma/updater -text
@@ -49,11 +43,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
+2 -6
View File
@@ -29,15 +29,11 @@ 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
- run: ./tools/op.sh setup
# Build
- name: Build docs
run: python3 docs/serve.py --build
run: ./tools/op.sh docs --build
# Push to docs.comma.ai
- uses: actions/checkout@v7
-69
View File
@@ -1,69 +0,0 @@
name: iOS companion
on:
push:
branches: [SecretGoodStarPilot, secretgood-ios-integration]
paths:
- 'openpilot/starpilot/companion/ios/**'
- 'openpilot/starpilot/galaxy_bluetooth/**'
- '.github/workflows/ios-companion.yaml'
pull_request:
paths:
- 'openpilot/starpilot/companion/ios/**'
- 'openpilot/starpilot/galaxy_bluetooth/**'
- '.github/workflows/ios-companion.yaml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-companion-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
app:
name: Swift tests and iOS Simulator build
runs-on: macos-26
timeout-minutes: 25
defaults:
run:
working-directory: openpilot/starpilot/companion/ios
steps:
- uses: actions/checkout@v4
with:
submodules: false
lfs: false
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Run companion regressions
env:
CI_PRIMARY_REPOSITORY_PATH: ${{ github.workspace }}/openpilot/starpilot/companion/ios
run: sh ci_scripts/ci_post_clone.sh
- name: Build for iOS Simulator
run: >-
xcodebuild -project GalaxyBluetooth.xcodeproj
-scheme GalaxyBluetooth -configuration Debug
-sdk iphonesimulator -destination 'generic/platform=iOS Simulator'
-derivedDataPath "$RUNNER_TEMP/galaxy-ios-build"
CODE_SIGNING_ALLOWED=NO build
bridge:
name: Comma Bluetooth bridge tests
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
submodules: false
lfs: false
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install bridge test dependencies
run: python -m pip install pycryptodome
- name: Run bridge regressions
run: python -m unittest discover -s openpilot/starpilot/galaxy_bluetooth/tests -v
+8 -194
View File
@@ -34,11 +34,6 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v7
- name: Validate published device build receipt
if: github.event_name == 'push' && github.ref == 'refs/heads/SecretGoodStarPilot'
run: python3 tools/ci/check_prebuilt_receipt.py --revision HEAD
- name: Check shipped device runtime dependencies
run: python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
- name: Validate tracked dependencies
run: python3 tools/vendor/check.py --revision HEAD
- run: ./tools/op.sh setup
@@ -201,8 +196,13 @@ jobs:
timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }}
env:
RAYLIB_BACKEND: headless
STARPILOT_TEST_DIAGNOSTICS: ci-results/host-test-diagnostics/unittest
run: tools/op.sh test --json-output unit-test-results.json
- name: Test custom module-level pytest cases
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 15
env:
RAYLIB_BACKEND: headless
run: python3 tools/ci/run_host_tests.py --pytest
- name: Test GM vehicle configurations and CAN behavior
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 5
@@ -227,139 +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('ci-results/host-test-diagnostics/**') != '') }}
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '') }}
uses: actions/upload-artifact@v4
with:
name: unit-test-results
path: |
unit-test-results.json
gm-test-results.json
ci-results/host-test-diagnostics/
if-no-files-found: error
custom_pytest:
name: custom pytest (${{ matrix.shard }})
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
steps:
- uses: actions/checkout@v7
- name: Provide JavaScript runtime for Galaxy contract tests
uses: actions/setup-node@v7
with:
node-version: '24'
package-manager-cache: false
- name: Provide pinned map provider toolchain
uses: actions/setup-go@v7
with:
go-version: '1.25.1'
cache-dependency-path: mapd_repo/go.sum
- name: Fetch pinned map provider dependencies
working-directory: mapd_repo
env:
GOTOOLCHAIN: local
run: go mod download
- run: ./tools/op.sh setup
id: setup
- name: Build openpilot
id: build
run: scons
- name: Provide H.264 encoder for Android Auto integration tests
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: |
uv pip install --python .venv/bin/python --only-binary av av==19.0.1
.venv/bin/python -c "import av; assert 'libx264' in av.codecs_available"
- name: Test every assigned custom pytest node serially
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 15
env:
RAYLIB_BACKEND: headless
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}"
- name: Save collection, every outcome, JUnit and durable worker diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: custom-pytest-${{ matrix.shard }}
path: ci-results/custom-pytest-${{ matrix.shard }}/
if-no-files-found: error
custom_pytest_complete:
name: complete custom pytest catalog
needs: custom_pytest
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
pattern: custom-pytest-*
path: ci-results/custom-pytest-shards
- name: Require every case passed or audited capability-unavailable
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json
- name: Save complete custom catalog verdict
if: always()
uses: actions/upload-artifact@v4
with:
name: custom-pytest-complete
path: ci-results/custom-pytest-complete.json
if-no-files-found: error
aol_lifecycles:
name: AOL lifecycles (${{ matrix.shard }})
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
env:
FUZZ_SEED: '0'
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build native host dependencies
run: scons
- name: Gate every assigned lifecycle module serially
timeout-minutes: 10
run: python3 tools/ci/run_vehicle_tests.py --suite aol-lifecycle --shard-index "${{ matrix.shard }}" --shard-count 16 --output "ci-results/aol-lifecycle-${{ matrix.shard }}"
- name: Save every lifecycle outcome and native build
if: always()
uses: actions/upload-artifact@v4
with:
name: aol-lifecycle-${{ matrix.shard }}
path: ci-results/aol-lifecycle-${{ matrix.shard }}/
if-no-files-found: error
aol_lifecycle_complete:
name: complete AOL lifecycle catalog
needs: aol_lifecycles
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
pattern: aol-lifecycle-*
path: ci-results/aol-lifecycles
- name: Require every full-catalog case exactly once and passed
run: python3 tools/ci/lifecycle_shards.py ci-results/aol-lifecycles --count 16 --output ci-results/aol-lifecycle-complete.json
- name: Save complete catalog verdict
if: always()
uses: actions/upload-artifact@v4
with:
name: aol-lifecycle-complete
path: ci-results/aol-lifecycle-complete.json
if-no-files-found: error
map_provider:
@@ -395,7 +269,7 @@ jobs:
strategy:
fail-fast: false
matrix:
suite: [interfaces, safety-debug]
suite: [interfaces, safety-debug, safety-release]
env:
FUZZ_SEED: '0'
steps:
@@ -421,57 +295,6 @@ jobs:
path: ci-results/${{ matrix.suite }}/
if-no-files-found: error
safety_release_contracts:
name: RELEASE vehicle contracts (${{ matrix.shard }})
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
env:
FUZZ_SEED: '0'
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build native host dependencies
run: scons
- name: Run every assigned RELEASE target serially
timeout-minutes: 20
run: python3 tools/ci/run_vehicle_tests.py --suite safety-release --shard-index "${{ matrix.shard }}" --shard-count 4 --output "ci-results/safety-release-${{ matrix.shard }}"
- name: Save every RELEASE outcome and native build
if: always()
uses: actions/upload-artifact@v4
with:
name: safety-release-${{ matrix.shard }}
path: ci-results/safety-release-${{ matrix.shard }}/
if-no-files-found: error
safety_release_complete:
name: complete RELEASE vehicle catalog
needs: safety_release_contracts
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
pattern: safety-release-*
path: ci-results/safety-release-shards
- name: Require every RELEASE case exactly once and passed
run: python3 tools/ci/lifecycle_shards.py ci-results/safety-release-shards --suite safety-release --count 4 --output ci-results/safety-release-complete.json
- name: Save complete RELEASE verdict
if: always()
uses: actions/upload-artifact@v4
with:
name: safety-release-complete
path: ci-results/safety-release-complete.json
if-no-files-found: error
recorded_vehicles:
name: recorded vehicles (${{ matrix.variant }})
runs-on: ubuntu-24.04
@@ -575,15 +398,6 @@ jobs:
with:
name: process_replay_diff.txt
path: openpilot/selfdrive/test/process_replay/diff.txt
- name: Save complete replay outputs for review
uses: actions/upload-artifact@v7
if: always()
with:
name: process-replay-outputs
path: |
openpilot/selfdrive/test/process_replay/fakedata/*.zst
openpilot/selfdrive/test/process_replay/coverage.json
if-no-files-found: error
- name: Upload diff report
uses: actions/upload-artifact@v7
if: always() && github.event_name == 'pull_request'
-5
View File
@@ -18,7 +18,6 @@ a.out
.venv-linux-arm64/
bin/
!openpilot/starpilot/galaxy/bin/
!openpilot/starpilot/soloist/pulseaudio/bin/
# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml)
/msgq
@@ -45,8 +44,6 @@ bin/
*.os-*
*.so
!openpilot/tools/replay/libmedia_mux.so
!openpilot/starpilot/soloist/pulseaudio/lib/*.so
!openpilot/starpilot/soloist/pulseaudio/modules/*.so
*.a
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
*.unchunked
@@ -141,5 +138,3 @@ openpilot/selfdrive/modeld/models/*.unchunked
/jetlink_repo/jetlink.egg-info/
# Local Google Play resolver build output (source and lockfile are versioned).
openpilot/starpilot/flm/_primitive_walk.cpp
+1 -9
View File
@@ -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
@@ -25,11 +25,3 @@ The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and For
## RAV4 Continental ARS510 receiver
The optional ACC-target and fused-track receivers and DBC preserve firestar5683's Dom integration from `b220a37f4bd119e402225a7aa16a7cd84878d0af` (Sea Turtles, 2026-10-13) and its ARS510 source attribution to ds-sebastian/ars510-radar `fb5b29c2b5999f34afeb6f7ac494a73949d66c67`, copyright 2026 ARS510 radar contributors. The complete MIT permission and warranty notice is retained in THIRD_PARTY_NOTICES.md. Startup/settings ownership is adapted to the current StarPilot architecture; no original firmware binaries or safety permissions are copied.
## Spotify Bluetooth output
Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selected speaker through a process-local ALSA configuration, leaving the system ALSA default, soundd and the warning speaker untouched. That approach is adapted from firestar5683's RoadScore Bluetooth output (`roadscore/prototype/bluetooth_output.py`, firestar5683/StarPilot `RoadScore` at `249eb88b3a397db847e4a595d7e23b7e85d329e5`). The Soloist player itself is never included: each user downloads it from Spotify, and the PulseAudio server that carries its audio is vendored unmodified from Ubuntu (see THIRD_PARTY_NOTICES.md).
## Tesla Pre-AP pedal control
The Pre-AP configuration, pedal codec/feedback, VirtualDAS feedforward table, grade compensation, torque-zero estimator and vehicle-specific gains preserve firestar5683's intentional Dom work, including `7837a3c1aab`, `aed0b7fdb9e9` and pinned `b220a37f4bd119e402225a7aa16a7cd84878d0af`. Original author attribution: firestar5683 <168790843+firestar5683@users.noreply.github.com>. Startup/calibration ownership, current upstream button/PID interfaces and native command bounds are adapted to the current StarPilot architecture; the old permissive native gateway is not transplanted. Existing source/license notices remain intact.
-202
View File
@@ -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.
+11 -87
View File
@@ -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.
[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue?logo=readthedocs&logoColor=white)](https://deepwiki.com/StarPilotHQ/StarPilot)
[![Discord](https://img.shields.io/discord/1387432184121393333?label=Discord)](https://firestar.link/discord)
[![Last Updated](https://img.shields.io/github/last-commit/StarPilotHQ/StarPilot/StarPilot)](https://github.com/StarPilotHQ/StarPilot)
[![Wiki](https://img.shields.io/badge/Wiki-StarPilot-blue?logo=wiki)](https://wiki.firestar.link)
[![iOS App Store](https://img.shields.io/badge/iOS-App_Store-black?logo=apple&logoColor=white)](https://apps.apple.com/ca/app/starpilot-bt/id6821039119)
[![Android Beta](https://img.shields.io/badge/Android-Join_Beta-green?logo=android&logoColor=white)](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
View File
@@ -329,7 +329,6 @@ SConscript([
'openpilot/selfdrive/ui/SConscript',
'openpilot/starpilot/audio/native/SConscript',
'openpilot/starpilot/models/runtime_bridge/SConscript',
'openpilot/starpilot/flm/SConscript',
])
# Replay is also the isolated on-device viewer engine.
-28
View File
@@ -112,31 +112,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## PulseAudio server for Spotify
`openpilot/starpilot/soloist/pulseaudio/` contains unmodified AArch64 binaries from Ubuntu 24.04 packages: PulseAudio 16.1 (LGPL-2.1-or-later), libltdl (LGPL-2.1-or-later), libtdb (LGPL-3.0-or-later), libsoxr (LGPL-2.1-or-later), liborc and speexdsp (BSD-style). The complete copyright and license notices are in that directory's `licenses/`, package hashes in `PACKAGES.sha512`, and the corresponding source is available from Ubuntu's archive for the exact versions listed in its README. The libraries are dynamically linked and may be replaced with `update.sh`.
## Galaxy Android companion tunnel
`openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64` is the upstream
[hev-socks5-tunnel 2.18.0](https://github.com/heiher/hev-socks5-tunnel/releases/tag/2.18.0)
static Linux ARM64 release, under the MIT license. Its notice is retained in
`openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel`; the adjacent SHA-256
file pins the downloaded artifact. The Android companion uses the Kotlin
standard library (Apache-2.0) and the Gradle wrapper (Apache-2.0); no third-party
UI framework is bundled.
## Galaxy Bluetooth companion source snapshot
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/StarPilotHQ/StarPilot
(natehuby76, PRs #224/#225) are included under `openpilot/starpilot/galaxy_bluetooth/`
and `openpilot/starpilot/companion/ios/`, with their license files (`UPSTREAM-LICENSE`, `openpilot/starpilot/companion/ios/UPSTREAM-GALAXY-LICENSE`).
The bridge runs on openpilot's Python with PyCryptodome 3.23.0 (BSD/public-domain
components, from opendbc). Its other dependencies are vendored unmodified, as
pure-Python sources, in `openpilot/starpilot/galaxy_bluetooth/vendor/`:
dbus-next 0.2.3 (MIT), ifaddr 0.2.0 (MIT) and Zeroconf 0.151.3
(LGPL-2.1-or-later). Their license texts are in `vendor/licenses/`, and
`vendor/VENDOR.json` records each PyPI source file and its SHA-256. Zeroconf is
kept as separate, unmodified source files that can be replaced with another
compatible version, as the LGPL requires.
-8
View File
@@ -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" "$@"
+1 -1
View File
@@ -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'
+1 -26
View File
@@ -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'),
}
+2 -8
View File
@@ -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,
+5 -10
View File
@@ -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 -6
View File
@@ -10,12 +10,7 @@ $Go.import("pfeifer.dev/mapd/cereal/custom");
# DO rename the structs
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
struct StarPilotCarControl @0x81c2f05a394cf4af {
# The first three fields are pointer slots mapd does not read.
hudControl @0 :AnyPointer;
steeringLimitInfo @1 :AnyPointer;
navigation @2 :AnyPointer;
phoneGps @3 :StarPilotCarState.Gps; # sourceMonoTime is comma receive time minus the phone's fix age
struct CustomReserved0 @0x81c2f05a394cf4af {
}
struct CustomReserved1 @0xaedffd8f31e7b55d {
+331 -405
View File
@@ -9,137 +9,69 @@ import (
math "math"
)
type StarPilotCarControl capnp.Struct
type CustomReserved0 capnp.Struct
// StarPilotCarControl_TypeID is the unique identifier for the type StarPilotCarControl.
const StarPilotCarControl_TypeID = 0x81c2f05a394cf4af
// CustomReserved0_TypeID is the unique identifier for the type CustomReserved0.
const CustomReserved0_TypeID = 0x81c2f05a394cf4af
func NewStarPilotCarControl(s *capnp.Segment) (StarPilotCarControl, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4})
return StarPilotCarControl(st), err
func NewCustomReserved0(s *capnp.Segment) (CustomReserved0, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
return CustomReserved0(st), err
}
func NewRootStarPilotCarControl(s *capnp.Segment) (StarPilotCarControl, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4})
return StarPilotCarControl(st), err
func NewRootCustomReserved0(s *capnp.Segment) (CustomReserved0, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
return CustomReserved0(st), err
}
func ReadRootStarPilotCarControl(msg *capnp.Message) (StarPilotCarControl, error) {
func ReadRootCustomReserved0(msg *capnp.Message) (CustomReserved0, error) {
root, err := msg.Root()
return StarPilotCarControl(root.Struct()), err
return CustomReserved0(root.Struct()), err
}
func (s StarPilotCarControl) String() string {
func (s CustomReserved0) String() string {
str, _ := text.Marshal(0x81c2f05a394cf4af, capnp.Struct(s))
return str
}
func (s StarPilotCarControl) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
func (s CustomReserved0) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
return capnp.Struct(s).EncodeAsPtr(seg)
}
func (StarPilotCarControl) DecodeFromPtr(p capnp.Ptr) StarPilotCarControl {
return StarPilotCarControl(capnp.Struct{}.DecodeFromPtr(p))
func (CustomReserved0) DecodeFromPtr(p capnp.Ptr) CustomReserved0 {
return CustomReserved0(capnp.Struct{}.DecodeFromPtr(p))
}
func (s StarPilotCarControl) ToPtr() capnp.Ptr {
func (s CustomReserved0) ToPtr() capnp.Ptr {
return capnp.Struct(s).ToPtr()
}
func (s StarPilotCarControl) IsValid() bool {
func (s CustomReserved0) IsValid() bool {
return capnp.Struct(s).IsValid()
}
func (s StarPilotCarControl) Message() *capnp.Message {
func (s CustomReserved0) Message() *capnp.Message {
return capnp.Struct(s).Message()
}
func (s StarPilotCarControl) Segment() *capnp.Segment {
func (s CustomReserved0) Segment() *capnp.Segment {
return capnp.Struct(s).Segment()
}
func (s StarPilotCarControl) HudControl() (capnp.Ptr, error) {
return capnp.Struct(s).Ptr(0)
// CustomReserved0_List is a list of CustomReserved0.
type CustomReserved0_List = capnp.StructList[CustomReserved0]
// NewCustomReserved0 creates a new list of CustomReserved0.
func NewCustomReserved0_List(s *capnp.Segment, sz int32) (CustomReserved0_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz)
return capnp.StructList[CustomReserved0](l), err
}
func (s StarPilotCarControl) HasHudControl() bool {
return capnp.Struct(s).HasPtr(0)
}
// CustomReserved0_Future is a wrapper for a CustomReserved0 promised by a client call.
type CustomReserved0_Future struct{ *capnp.Future }
func (s StarPilotCarControl) SetHudControl(v capnp.Ptr) error {
return capnp.Struct(s).SetPtr(0, v)
}
func (s StarPilotCarControl) SteeringLimitInfo() (capnp.Ptr, error) {
return capnp.Struct(s).Ptr(1)
}
func (s StarPilotCarControl) HasSteeringLimitInfo() bool {
return capnp.Struct(s).HasPtr(1)
}
func (s StarPilotCarControl) SetSteeringLimitInfo(v capnp.Ptr) error {
return capnp.Struct(s).SetPtr(1, v)
}
func (s StarPilotCarControl) Navigation() (capnp.Ptr, error) {
return capnp.Struct(s).Ptr(2)
}
func (s StarPilotCarControl) HasNavigation() bool {
return capnp.Struct(s).HasPtr(2)
}
func (s StarPilotCarControl) SetNavigation(v capnp.Ptr) error {
return capnp.Struct(s).SetPtr(2, v)
}
func (s StarPilotCarControl) PhoneGps() (StarPilotCarState_Gps, error) {
p, err := capnp.Struct(s).Ptr(3)
return StarPilotCarState_Gps(p.Struct()), err
}
func (s StarPilotCarControl) HasPhoneGps() bool {
return capnp.Struct(s).HasPtr(3)
}
func (s StarPilotCarControl) SetPhoneGps(v StarPilotCarState_Gps) error {
return capnp.Struct(s).SetPtr(3, capnp.Struct(v).ToPtr())
}
// NewPhoneGps sets the phoneGps field to a newly
// allocated StarPilotCarState_Gps struct, preferring placement in s's segment.
func (s StarPilotCarControl) NewPhoneGps() (StarPilotCarState_Gps, error) {
ss, err := NewStarPilotCarState_Gps(capnp.Struct(s).Segment())
if err != nil {
return StarPilotCarState_Gps{}, err
}
err = capnp.Struct(s).SetPtr(3, capnp.Struct(ss).ToPtr())
return ss, err
}
// StarPilotCarControl_List is a list of StarPilotCarControl.
type StarPilotCarControl_List = capnp.StructList[StarPilotCarControl]
// NewStarPilotCarControl creates a new list of StarPilotCarControl.
func NewStarPilotCarControl_List(s *capnp.Segment, sz int32) (StarPilotCarControl_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}, sz)
return capnp.StructList[StarPilotCarControl](l), err
}
// StarPilotCarControl_Future is a wrapper for a StarPilotCarControl promised by a client call.
type StarPilotCarControl_Future struct{ *capnp.Future }
func (f StarPilotCarControl_Future) Struct() (StarPilotCarControl, error) {
func (f CustomReserved0_Future) Struct() (CustomReserved0, error) {
p, err := f.Future.Ptr()
return StarPilotCarControl(p.Struct()), err
}
func (p StarPilotCarControl_Future) HudControl() *capnp.Future {
return p.Future.Field(0, nil)
}
func (p StarPilotCarControl_Future) SteeringLimitInfo() *capnp.Future {
return p.Future.Field(1, nil)
}
func (p StarPilotCarControl_Future) Navigation() *capnp.Future {
return p.Future.Field(2, nil)
}
func (p StarPilotCarControl_Future) PhoneGps() StarPilotCarState_Gps_Future {
return StarPilotCarState_Gps_Future{Future: p.Future.Field(3, nil)}
return CustomReserved0(p.Struct()), err
}
type CustomReserved1 capnp.Struct
@@ -3395,312 +3327,306 @@ func NewMapdOut_GpsSource_List(s *capnp.Segment, sz int32) (MapdOut_GpsSource_Li
return capnp.NewEnumList[MapdOut_GpsSource](s, sz)
}
const schema_b526ba661d550a59 = "x\xda\x94Z}lT\xd7\x95?\xe7\xbd\xb1\xc7\x9f\x8c" +
"\x877\x04\x0c&\x13(\xd0\x98\xc6i\x8c\xf16\xa6I" +
"]c\x03\xc1\x1a\x83\xc7\xc3G@A\xea\xf5\xbc\xeb\x99" +
"\x07\xcf\xefM\xde{c{\x10\x11\x84\x82\x04YP\x93" +
"n\xd2\x92*lC\x02R\xda\x0d-\xed\x86\x15\x1bQ" +
"-AD\"Y\x90\x92\xaa\xa9\x9a4\xbbI\xa3JI" +
"\xba\x89\xd2v\x13m\xb6\xdb\xe8\xad\xce\xbd\xf3\xf1\xfc\xd1" +
"\x10\xff\xc3\xf8\xfd\xcey\xe7\x9e{\xee\xb9\xe7\x9e\xdf}" +
"\xdc\xf1\xf5\xfao\x86\xda\x1b3\x0d\xa0$\x1f\xae\xaa\xf6" +
"\xa3{v\xbccy?9\x00\xd1E\xe8\xef\xa8\xdb\xba" +
"x\xe4\xf9\x15\xe7!\x14\x06\xe8h\xae\xde\x89Z[u" +
"\x18T\xff\xa7\x1f'\xbav\xfe\xf1\xf2\x83\x93\xb5\xaa\x84" +
"Zm\xf5^\xd4\x16W\x87\x01\xb4\xe6\xeaw\x01\xfd{" +
"{/\x7fgx\xc5\x0f\x0fCt\x91RQ\x06\xec\xc0" +
"p?j\xf3\xc2\xf4R4l\x87\x00\xfd\xb6\x97\xf2\xf7" +
"\xfe\xb8\xf9\xf8#\x90l\xc3Z\xff\xb2\xf5o\xf5\xdb\xae" +
"\xdc\xf7\xdfP\x85\xa4s\xbc\xfe!\xd4\xce\xd4\xd3\x9f\xa7" +
"\xea\xdfR\x00\xfd\x7f\xfc\xcaGyc\xef\xce\xefA\xf4" +
"K\x8a\x7f\xf0\x93O\xbe\xd4\xf1_\x7f:M\x96_\x9f" +
"s\x10\xb5\x0f\xe7\x90\x0f\xef\xcf\xf9>\xa0?\xf79\xcc" +
"d\xae^\x7fb\x86iuE\x86Q\x1b\x88\xd0\xb4\xda" +
"~\x1bW\xfb\xc2\xd9S3h\xb5Fv\xa2v\xb7\xd0" +
"2\xd5\x8eo~:\\\xfd4$\x17\xe1\xb4\xd97\x93" +
"^[D\xbe\xb2\x1d\x01+~%\x17\xe1\x9c\x80:E" +
"\xa8\xe3d\xd3B\xd4\xce6}\x19\xa0\xe3\xa5\xa6\xf10" +
"\xa0\xdf\xd7{\xe9\xe5\xb3o=}fZ\xb4\x92\xcd\x07" +
"Q\xe3\xcd4'\xd6\xfc5@\x7f\xeb\xe9\xe4\xeb\xb7\x8d" +
"]?3\x83\xb7\xbcy'j\x85f\xf2\xf6'\xbfM" +
"?\xf6\xf7\xcf|\xf4O\xd3\xecmm^S\xb1\xb7\x19" +
"\xd0\xdfu\xfe\xdd\xf6\xef|\xf6\xd6Of\xb0\x97'{" +
"G\x85\xbd\xee\xb7\xfe\xf0\xc0\xc0-\xdb\xcf\xcd8\xeap" +
"i\xd4\xab\x98Xb\xa4\xad\x0b3hm%[\x86\xd0" +
"\xca^\xfc\x87GS_\xef\xfb\xc5\x0cZ\xeb\xc8\xd6\x0e" +
"\xa1uH]\xfd6\xbfk\xd9\xe5\x19\xb4:Ik\xa3" +
"\xd0\xfa\xe8\x9f\xbf\xf1\xea\x9dw\xef\xbe:uU\x14\xd2" +
"[\xde<\x17\xb5N\x9akG{s\x9cVe\xcd\x8e" +
"\xed9\xf3\xb5\x1f\xfe\xfbLC/\xa4\xa1\x17\x92\xd1W" +
"\x0e>\x99\xf9\xbf7\xbew}\xa6\xa1Ik\xa3\xd0Z" +
"|%\xfd\xe6\x9b\xde\xc1W\xa7\x85x\xf9\xc2\xb5\xa8u" +
".\x14\xc3.\x14\xc9\xd0\xd5u\xf1\xd4\xb5\x87\xfe\xe7\xd7" +
"\xe4\xa5:\xc5\xe2\x92E\xfd\xa8u.\xa2\x05i_D" +
"\x1b\xa73\xd1\xf7\xed\x13\xdb\xbf\xf7\xc6\x0c\xa3\xcfk\xd9" +
"\x89Zk\x0b\x8d\xde\xf5\xed\x97\x7f\xf0\xfd\xaa\xef\xff'" +
"\x99T\xa6\xa8U\xb5\xacEm^\x0b\x99\x8c\xb6\x8c\x03" +
"\xfa\xb7\xbe\xfa\xeb\x8fn[\xf7\xdc\x9f\xa7\xed\x98|\xcb" +
"n\xd4\x8e\x0a\xcd\xc3-\x19\xc0\xca\xe6\x9blVn\xc5" +
"\x9f\x91\xf6\x8b-\xf3\x01:\xae\xb7\x84)m\x9bO\x9e" +
"\xdcX\xfb\xc1M\x1f\xcf\xe0\xebk\xcbv\xa2\xf6\xfe2" +
"\xf2\xf5\xe9P\xee\xb3\xaf\x1f:\xfe\xbf3h\xbdHZ" +
"\xaf\x0b\xad\x95\xef-\xdf\xf4\xee}\xdf\xf8\xcb\xb4x\x9e" +
"_6\x8c\xdaK\xcb\xc8\xc9\x17\x97\xed\x07\xf4\x9fr\x07" +
"\xaf\xbe\xbc\xeb\xe9\xbfLuR,z\xd5\xf2\x83\xa85" +
"/\x17\xe1Z.\xa2\x1f\xfd\x97\xf1#\x1f\xf6\x0c\xffu" +
"\x86\xe1\x1f[1\x8c\xda\x8fV\xd0\xf0\xfbO\x9c{7" +
"u\xe2\x88?5\x93\xc4\xcc\x0f\xafx\x1e\xb5\x93+\xc8" +
"\x85\xc7W\xfc\x14\xda\xfc4w83\xbf\x9a\x0e\xe5]" +
"\xcf\x1e\xfdjZ\xfc\xdc\x9ef9+\xb7\xa6W<\x0c" +
"q\x97;c\\\xd5W\x0f\"~\xae~\xcac\xce\xa0" +
"a\xda^/sz\xbbm\xcbsls\x101\xd9\xa4" +
"\x86\x00B\x08\x10e;\x01\x92\xdfR1i*\x18E" +
"\x8c!\x81\xc6w\x01\x92\xa6\x8a\xc9\x09\x05\xa3\x8a\x12C" +
"\x05 \x9a'MO\xc5\xe4\x01\x05\xa3\xaa\x1aC\x15 " +
"\xfa@?@r\x9f\x8a\xc9#\x0a\xfa\xd9\xbc\xdeKc" +
"\x80j\x9b8\x17\x14\x9c\x0b\xe8\xbb\x1e\xe7\x8eae0" +
"a\x8c\x1a\xdeFk\x04\xed\xb2\xccbcF\x86y\x06" +
"\xa8\xb6U\x06sY\xdb\xe2\x1br.\x00`S\xa5\x80" +
"\x03b\x13|\xfel\x07XN\xdfh\xe5\xf2\xde\x96B" +
"\x8e\x03\xd0D/\x92\xebZ\x9b\xd2\x0f\x80s\xb4V\xe5" +
"\xe7\x00\x18\xd1Z\x95\xa7\xc8\xb8\xd6\xaa\xfc\x18\x00\xa3Z" +
"\xabr\x19\x00\xe7j\xad\xca\x1b\x00X\xaf\xb5)\xc3\x00" +
"\xa8i\xad\xca\xcb\x00\x18\xd3\xda\xc4/j\xed\xca^\x00" +
"T\xb46e7\x00\xce\xd3Z\xc5\xf3M\xdar\xe5\x0f" +
"\x008_kU~\x09\x80\x0b\xb46\xe5\x1d\x00l\xd6" +
"\xda\xc5\xf3B\xadS\xf9\x01\x00.\xd2:\xc5\xb8-Z" +
"\xa7\xb0\xb7X\xeb\x12\xcf7k]\xc2/\xb5\xf8\x1c\xd2" +
"\xba\x84?q\xadK\xd8\xbdE\xbb[\xd8[\xd2\xd1\xa3" +
"\xacA\x00\xac\xd26*\xcf\x03`\xb5\xb6Q8\xd0\xa8" +
"\xadSv\x02`\x83\xd6#\x1c[\xaa\xdd-^\xfc\x92" +
"\xd6#\x0c/+\xfe.\xef\xe8Q\xe6\x92\x81:m\xa3" +
"\xf2\x10\x00\xae\xd06*\x7f\x06\xc0/w\x0c(KI" +
"p\xab\xb6U\x84\xa0\xb5c\x87\xa2\x10\xb0\xb2c\x97\xfc" +
"\xe3+\x1aS\xbe\x0b\x80\xb7iL\xbc\xda\xa61\x11\xbc" +
"p\xf1\xb9Fc\xcaA\x00\xac\xd5v\x89\xdf\xdb\xb5\x1d" +
"\xe4\x83\xaf\xdb\xe3\x96i3\x1d\x00|\x97{[\x98\x93" +
"\xe1\xe8%\x98\xc7\x1df\xc6{\xd2in\x12\x9e\xcaq" +
"\xae\xcb\x14\xd9<2\x12v\xb97\x05\xed\xb5\xad\x08e" +
"/\xc1\x03,\xd7\x9b\xafr\xc6\xb8\x90\xf7\xca\xb4\x06\x97" +
"{\xdb\x0c\xd7\xb0\xad\xde\xfc$\x91*_J\xd8\x99\x04" +
"\x87\xf0\x98\x1cOh*RU\xf8D.\xf5H\x1f\x83" +
"\xb2\x01\xc3\x92\xe2m\x00\xbe\xc3i&)\x0e\xdd\x9eg" +
"X\x19\xd7w\xd9\x18Oq\xcf\x83\x88|\xe4\xde:\x8b" +
"\x0d\x9b\xd0-\x87\x9fjl\xab\xcb\x85\x9c\xa7\"%\xb1" +
"\x98\x8a2I\x96\xe3\x1c\xf5\xf2\xec\x151\xfb\x804\\" +
"|\xf3\x1e\xdb\xd4\x13\x0as\xbd\x14\xe7\x96P%M\xf4" +
"\x02Q\x16h?W\x9d=S\xc1\x9et\xb8\x18x\x81" +
"*\x12\xddb\x8c\xf2\xcd##.\xf7d \xfa\xf8\x08" +
"\xcb\xa3\xe9%\x98\xc5\xb7\x1ba\xdd\xcb\x96]\xc6R\xdc" +
"\xe2\"p>\x05\x86\xd41oz\x14\x11#L\x01!" +
"t\x88\xa7\xed\xaa\xd1Qn\xe9\\\x17\x12+\xe3\xd2Z" +
"\xa5L{\xbc\xcf\x1e\xb7\xd6\xdb\xce&>!\x1dHD" +
"h\xb2\x95\xb9o\xcdM\x92\x1a\xe1\xa2\x94\xe6\x9eRK" +
"s\xf6\xb6g\x0d\x93\xf7f\x99\x951\xacL\x8awK" +
"u1\xfa w\\4\\\x8f[\x1e\x09\xe4\xb2\xa5\x99" +
"\x95\xe6f\x9f\x0d\xdd27\x8b\xe9\xd1\xefR\x1d*>" +
"\xa4l\x88\xe4\x9d4\x17\x8b:\xe1qG\xb1\x98Y\x0e" +
"\xf3\xa4t\x14b,\x89\xe3\x89Is\x90\xd9;\xe8\x18" +
"q\xdb1\xbcB\x19W\xa5\x19r\x9a\x0f\xf1\xfb\xf3\x86" +
"\xc3]\xda\x0d9\xf4|F\xbf^*\x87\xa5\xd1\xe4r" +
"\x0c:\xdcu\x95\x0d\xcc\xddb\xf7\x145&\x8d\xd7\xa3" +
"\xef\xce\xbb*\x85_\xaefP\xab\x12;\x01*^e" +
"*\xb4\xea\xb6\x9a\xf7\xcaC\xd4\x8a!6\x8fq\xc71" +
"t^Q\xa4U\xeb\xb5-\xdd\xf0\x0c{j0J;" +
"0\x95e\xba=\xde\xcb\x9c\x94\xc7<\x14\xd1\x13\x10\x8e" +
"\x0f\xd8:7\xb7\xad\x92S)b\x1brn\xc2N\xb3" +
"\x08\x19$\xb8\xdf\xb5\xadAd^v\xbdi\xb3\xe2\xac" +
"\x05V\xcd\xbc\xec\x16>!\\\x10\x00\xf3\xb2k\xed\xe0" +
"\x90ESd\x89\xd6#\xe2X\xcc,\x1f\x16U78" +
"\x1a\xc9Y~\xbb8s\xe8\xd0X]:\x1d\xb5]\xb8" +
"\x17 u\x1f\xaa\x98\xcab\xe5\x80\xd48\xf6\x03\xa4t" +
"\xc2sX9#\xb5Q\x1c\x02H\x99\x84O`\xe5\x98" +
"\xd4\xf2B\xdf#\xfc\x00\xe1\xa1\x9a\x18\x86\x00\xb4\x07p" +
"\x15@j\x82\xf0C\x84W\xd5\xc6\xb0\x0a@{\x10w" +
"\x02\xa4\x0e\x10~\x8c\xf0\xea\xba\x18V\x03hG\xf1\x07" +
"\x00\xa9c\x84\x9f <\\\x1f\xa3&B{\x0c\x1f\x02" +
"H\x9d \xfc4\xe15\x0d1\xac\x01\xd0N\x09\xfd\xd3" +
"\x84\x9f#\xbc\xb61\x86\xb5\x00\xdaYt\x00R\xcf\x12" +
"~\x81\xf0\xbap\x0c\xeb\x00\xb4\xf3\xf8\x14@\xea\x02\xe1" +
"WP\xc1%\xf5J\x0c\xeb\x01\xb4\x17p\x0d@\xea\"" +
"\xe1WQAl\x88a\x03\xf5L\xb8\x12 u\x89\xe0" +
"7QA\xdf\xb5i\xcb\x0c\xd8\xd0m\xd9\x94VX\x0b" +
"\x0a\xd6\x02\xfa&\xf3\x0c/\xafs:\xd3\xebA\xc1z" +
"\xc2l+C /c\xcc\x9c\xa6\x17w\xc56\xaa" +
"\x03\x05\xeb\x00\xfda\xce\xa8\x9b\xe8\x03\x95g\xca`\xd6" +
"v\x8c\xbd\xb6\xe5!3{\xd2\xe9\xbc\xc3\xd4t\xa1," +
"\x1c\xe3\x8eg\xa4\x99\x89R\x94.\xd0\x018\xd9Z\x0f" +
"\x16E}A\xab\xae\xac\x94i\x88\x0bY\x19\xcf[\xc6" +
"\x84\x98\x9d\xeb\xb1\xd1\xdc\x80a\x86M\xc3\xc5*P\xb0" +
"\x0a\xb0;\xcb\xdc\xf5\xc6\x04\"(\x88\x80\x91\xb1M\xeb" +
"\xfap\x0e\xe0\xa0\x8a\xc2\xc0\x9c@\x0fS\xf57z\x98" +
"\xcdy\xef\xf6\x14\x1b\xcd\x99\x9c\x123\xef\x8a\xa4\\ " +
"\x9a\xb0\x9e\xb5\xd4\x88D\xbbVQ\x1f\x12m\xa7S_" +
"\x8d\xb6\x11\x18\x8a.\xa7^\xa0*\xbad7\xc0\xfe\xbc" +
"\xb5\xc7\xb2\xc7\xad\xb8eo\xc8\xb9\xbee\xf7\xdac\xdc" +
"a\xa0f\xf8~\xcb\x1e`^:\xeb\x8f\xd2\xbf\\\xdf" +
"\x04\xdd\xb6,!E \x01\xb2\x0a\x7f\xe1>4\xac\xb7" +
"\xaf\xbaQ#:\xa5q\xed\xba\x91>\x85\x81\xf61\x9d" +
"\x1a\x9b\xd5\xbc'\"PnZ\x1f\x7f\x08 \xf9\x84\x8a" +
"\xc9g\x02M\xeb\x19jEO\xab\x98<\x17hZ\xcf" +
"\xae\x04H>\xa3b\xf2J\xa0i}\x814/\xa9\x98" +
"\xbc\xa6 \x86\xc4N\x8c\xbet\x10 yU\xc5\xe4\xaf" +
"h\x1b\xa2\xd8\x86\xd1W\x87\x01\x92\xaf\xa8\x98|S\xc1" +
"r'\x83\x83\x8e\x9d\xa1:)\xfa\xd32a(\xf6\xa7" +
"n\xe9\x88\xa3~\x0c\x14l\x00\x8c\xe4\x98\x97-\xe5@" +
"S\x85\xb1\x01\x8al\xc8\xd9\xae\xa8\xa8\xb2\xdf-\x93\xaf" +
"\xa2=\xd3\xb6sC\xcc\xe3\xd8C\x0b\x98\xe1\x95\xdc-" +
"I <`Xe\xb4\x14T\xf5o\xe5\x96\x9a\xf7\x92" +
"5\x18\xbc\xea\xa8\xdd\x1d`q\xb5C~)\xf5 B" +
"\xc9\xe7o\xc8\xb9)\xda\xd7\x80<y\xa4\\\x19\x7f\xa4" +
"\xae\xa5\xca\xa2ReQ\x03\x95\xf1\xacJ\x95\xe2\x19\xc2" +
"\x9fS\x03\x95\xf1g*U\xc0s\x84_T\x15\xc4b" +
"a\xfcW\x95\x0a\xdd\x05\x82\xaf\x90z\x08ea|A" +
"\xa5\xc2{\x89\xf0k\x84W)\xb20\xbe\xa4>\x0f\x90" +
"\xbaF\xf8o\x08\xafVea|M\x0c\xfb\x0a\xe1o" +
"\x12\x1e\x0e\xc9\xc2\xf8\xba\xb0\xff\x1b\xc2\x7fOx\x8d*" +
"\x0b\xe3\xefT*\x8c\xbf'\xfc\x8f\x84\xd7\x86da\xfc" +
"P\xa5\xc2\xf8\x01\xe1\x9f\x12^W%\x0b\xe3'\xeaw" +
"\x01R\x9f\x12\x1e\x0a)\x18\xad\xaf\x96\x85\x11C\xbf\x04" +
"H\xd5\x84TL\xc5\x08ox[V\xc6h\x88\xfci" +
" |\x01\xe1\x8d\x8bc\xd8\x08\xa0\xcd\x0bQ\xc1o\"" +
"\xbc\x85\xf09\xbf\x8b\xe1\x1c\x00\xad9D~. |" +
"\x19\xe1\x91\x9a\x18F\x00\xb4%\xa1\x97\x01R\xb7\x12\xbe" +
"\x9a\xf0\xa6\xda\x186\x11\x9f\x0fQ|\xee \xfc.\xc2" +
"\xa3u1\x8c\x02h]!\x9a\xd7]\x84\xdfC\xf8\xdc" +
"H\x0c\xe7\x02h\xebB\xc3\x00\xa9>\xc2\x07\x09\xd7\xea" +
"c\xa8\x01h\x03\xa1\x9f\x03\xa4\x06\x09\xbf\x8f\xf0XC" +
"\x0cc\x00\xda\x8e\x10\x1d(\xf7\x11\x9e%|^c\x0c" +
"\xe7\xd1\xc1\x17\xa2\xf8\xe8\x84\xe7\x08\xbf\xa9%\x867\xd1" +
"\xc1'\xf4s\x84\xef#|\xfe;1\x9c\x0f\xa0\x15\x84" +
"?\xfb\x08?B\xf8\x82\xc51\\\x00\xa0\x1d\x0e\xed\x06" +
"H\x1d\"\xfca\xc2\x9bkb\xd8\x0c\xa0\x1d\x17\xf19" +
"B\xf8\xa3\x84/\xac\x8a\xe1B\x00\xed\x11\xe1\xe7\xa3\x84" +
"?I\xf8\xa2\x9bc\xb8\x08@;)\xfcy\x82\xf0g" +
"\x08o\x89\xc7\xb0\x05@;#\xe2y\x9a\xf0s\x84/" +
"^\x16\xc3\xc5\x94\x9e!:\xa0\x9f%\xfc\x02\xe17\xd7" +
"\xc5\xf0f:\xf8D|\x9e#\xfc\x12\xe1\xf1\xfa\x18\xc6" +
"\x01\xb4_\x88y]\"\xfc\x1a\xe1\xb74\xc4\xf0\x16\xca" +
"C\x81_#\xfc7\x84/i\x8c\xe1\x12\xca\xc3\xd0A" +
"\x80\xd4\xaf\x08\x7f;\xa4\xe0\xfeqV\xd8\xc4Fy\xa9" +
"\x14t\x8f\xb3\xc2\x10\x1f)=\xfa\x8e\xcdt\x92\x07\xaa" +
"\x85<v\x12\xc6(\xa8\x86W\xde\xd6V\xb1\x09\x86n" +
"\xd9rM\x13\xa0\xc4\xfb\x8cn\xd7\xa3\xf6\xb6\xa4\xd0\x9d" +
"e{\x99\xa3\x97\xad\x93\xfe=l/\x03u\x06\x10\x1d" +
"\xbd\xcf\xa0\xf7\xd5\x8a\x01\x9f\xe9c\x86k;\x05\x88\xa7" +
"&\x9d\xc4\xf4R\x8f>f \x09\xa5\x0b\xd3dJI" +
"V\xb4\x9b\xc6\x8ac\xb6\xc5\xb7\xb3B\xe9\xa0\x8c\x9b\xcc" +
"\xe2.V\x83\x82\xd5\x80\xbeg\x98<A\xb4Q\xe5z" +
"I\xa5\x1c\x19\xc5\xf0R\xf9L\x86\xbb\x1e\xd7\x85\xf1\xc0" +
"\x89\xee\x16\x05\xd0\xadOv\x97\xbb\x9e1J\x85T\x1f" +
"\xb2\x99\xbe\xdd\xd0U/[\x16\xd2:P\x17\x0ba>" +
"\xe1a\xa4r\xc7\x09\x88\x11@_7\x8aQ]\xef\xd8" +
"\xa3\xdbY\xa17\xce-j\xfa\xcb=F\x91xb\x89" +
"y\x06<\x1a%\xbe\xe4\x8c\xf1\xa9\xf1\x1bg\x85\x147" +
"y\x1a\xa9\xf8\xcb\x1b\x0b\x8cT\xae\xaa\x8a#\x97\xe6\x8c" +
"\x86\xec\xdc\xbd`@\xb2F&;\xce\x0a\xbd\x101\x99" +
"\xebb\xa4rq(\xdf\x8e\x8f\xb3\xc2F\xbd\xd4\x9a\xf8" +
"\xe9b\xf3>\x85\xabT2O\x14\xfem\x1c\xe2\x0eM" +
"\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd3 \xe5\x93\xa3" +
"\xe8b\xa6r@`\xa4r\x94T\xa4\x03\xb6eo\x81" +
"p\xb0!L\xdb\xa3\xb9\xbc\xc7u\x142\x83v@Y" +
"&\xbb\xc8\x0d\x1c-\xee0y2\x96e9\xc7\xd6\xf3" +
"i\xee`\x8a\xbb\xe4cE\xf2\xb9\xbd~\x99\xaaHz" +
"+\xa2M-E\x8dh\x12\xa2kDSU\xbb\x16\x80" +
"v\x8fg\xa4\xf7\xe7\xb8\x93\xe6\x967\x9b\x9e\xa63\xd8" +
"\xd3\xcct\xfcR\xde\x89K8>!\xfa\x99\x061\xf8" +
"b\xd9\xd1\xcd[):\xba\xc6\xb5\x00\xfbG\x1c\xce\xc7" +
"Y!\x926\xbcB\xa9\x93\x9b\x8d'\xed\xb3\xea\xc6\xc2" +
"z\xfb\xeaY\xb6o_\x9b\xed\x00\x9d\xb3}\xa1\xfdF" +
"\xc1\x14w}hQ\x1cc\xe5\xbe\xf0\x01\xea\xf6&T" +
"L\x1e\x0a\xf4\x85\x0f\xae\xaa\\Qb\xb1-<\xbc\x14" +
" y@\xc5\xe41j\x0b\x9bd[x\x94\xde>\xa4" +
"b\xf2\xe1J#\x12=N\xbd\xe21\x15\x93'\x14\x8c" +
"x\x85\x1c\xa5x\xf9\xfbSq\x97\x8d\x10[-\xed\xeb" +
"\xb0\xeb9\xe5\xceo\xd8\xb6\xcd\xf2n\xdd]$\xae\xc1" +
"b?\x9b\x98\xdc1\xdb v\xdc\xe8\x85{\x8a\xf5\x83" +
"\xaa\x87\xd8\x0fw\x8a\xe8\x9c\x92)\xf9x\xbfH\xc9\xc7" +
"v\x0b\x92\xf1\xc8*A2\x8e\x0e\x09\x92q\x98T\xaa" +
"\xa3\x0f\x0c\x03`8Z \xb0&\x9awh\xa7F\xef" +
"\xa7\xf7\xea\xa2\xa3\xf4^}\xd4\xa0\x9f\x86('\xcd\xc6" +
"(\x0b\x90\x13\x7f\xd4\xf6lg\x9c\x15\x00\xa0\xf2w$" +
"aX{\xe2\x9e\x93\xb7\xf6\xf8\xe2\xdf\x84a\x01\xee\xd9" +
"\x9fs\x8cQ\xe6\x14\xfc\xe2o\x02\xc2\x86\xb5\xc7w9" +
"\x155\xe6\x00\x16*\x7f\xc7\x0bd\xc3\xf7\x88\xf81G" +
"\x98/\xff-\xcc\xfby+Ms6 2bp\xdd" +
"w\xb8k\xe8\x9c\xca\xbf\xc1L\xdf4\xc6\x0c+C\xfd" +
"\xaf\xc3\xb9wC\x9aBK:h\x1b\x96'/\x0f\x02" +
"W\xeb\xfd3]\xad\x0f\x01$\xb3*&=j\x8eC" +
"2\x1d\xef'0\xa7br\x1f\xa5c\x95L\xc7\xc2\xde" +
"J2\x7fa\x02\x9d\xce;c\xcc\xcb;P9h}" +
"O^`r\xe86m*)\xd3\xb8\xc2\x17\xd8\xf1\x1d" +
"_\x84\xb0\x0d\x96\x98\x8c(\xad\xe58\xb4R\x1cnU" +
"1\xb9:\x10\x87v\x9a\xf2\x1d*&\xef\x9a\xcd\xecn" +
"\xe0\x00\x11gAW\xc4\xd5\x9dX\x0d\x11\xdf\xd6\x95\"" +
"\xa1\x97\xc8\x84^\xdc/\x12\xbay)@\xc4\xb2-\xee" +
"\x1bt\x96[\xcc\xa4D\xe1\x13\xe5\xbf\xc3i\xe6|\xe1" +
"\xaf+\xa9\x88\xc7<\x9e\x0ca\xe0\x9bs\x14\x97\x867" +
"\xe4\xdc\xe4\x812k\xba_\xa5\xb67G4b_\x90" +
"5\x15\x04\xad\xd9G\xf8\x91 k:\xacR[z\x88" +
"\xf0\x87\x09WQ\xd2\xa6\xe3\xeaS\x00\xa9\x87\x09\x7fB" +
"\xd0&U\xd2\xa6\xc7\x85\xfd\x13\x84\x9f\x16\xb4)$i" +
"\xd3)\x95\xda\xd2'\x09\x7fV\xd0\xa6*I\x9b~$" +
"\xec<K\xf8\x05A\x9b\xaa%m:\xaf^\x9eD\xcb" +
"j\xc2\x926\xbd X\xdfE\xc2\xaf\x0a\xdaT#i" +
"\xd3\x8b\x82f]!\xfc\x15A\x9bj%m\xba.X" +
"_\x85\xae\xd5\xd7I\xda\xf4\x9a\xf0\xf3W\x84\xbfMx" +
"C\xbd\xa4M\xff!h\xd6\xdb\x84\x7f@xc\x83\xa4" +
"M\xef\xab\xd4\xb6\xbfG\xf8\xc7\x84\xcfi\x94\xb4\xe9O" +
"\x82\xc6}\xac\xaa8$X\xd3\x1c\xc9\x9a>\x13\xc3\xfe" +
"\x95\xd4k\x04k\x8aH\xd6T%\xba\xfc\x10u\xe7M" +
"\x8255I\xd6\xd4(XD\x85\xad\xcd\x8dJ\xd64" +
"/\xf4\xfc$V\xa6\xa9\x925-\x09\x91\x9b\xcb\x08\xbf" +
"C\xb0\xa6\xb9\x925\xb5\x09;\xb7\x11~\xa7`M\x9a" +
"dM\x9dB\xffN\xc2\xfb\x04k\x8aI\xd6\xd4#X" +
"M\x85\x95\xcd\x9f'Y\xd3\x80\xb0\x93 \xfc^\xc1\x9a" +
"n\x92\xaci\xab`\x17[\x08\xff\x96`M\xf3%k" +
"\xda%\xd8`\x85\xad-\xfc\xa6dM\\\x8c\x9b%\xdc" +
"\x13\xac\xa9G\xb2\xa6\xfb\x85\x9d\x0a[kY+YS" +
"A\xe0\x13\x84\x1f\x12\xac\xa9W\xb2\xa6\x07\x05~\x80\xf0" +
"c\x825\xf5I\xd6tT\xf8Yfk\x18\x97\xa4\xe9" +
"\x91\xd0R\x80\xd41\x82O\x84\x14\x14\xf7\xd9\xe6\xa0\xc3" +
"!\xe2\xba\x81\xee\x95\x99\xe3\xac\xe0n\xb6\xb0\xf85h" +
"]\xb7\xf8\xc4QQ\x18v\xd8\x1e\x9e02\x10\xcez" +
"n\x19\xd5\x99\x9b\x1d\xb6\x99\x83z\xe0f\xbb\\\xd3t" +
">\xe3X\xe5\x06^\xdcps\x1d\xa6K\x12\xb6\x95!" +
"ix\xa6\xf7\x94m\xdc)\x944\x82\xef\xef\xe7i{" +
"\x03gN\xf9\x8d\x11\xdbI\xf3^\x9b\x81\xeaze\xd0" +
"p\x07\x99\xb3\x87hAe\xe0\x1c\xcb\xbb<\xc1<\x88" +
"\xd0\xe4\xa7\xc0\xb6\x85\xa2\xfa\x19\x16C3@\x80l\xc7" +
"\xdb\xc0\x19`e<\xcfa##Fz\x00m]~" +
"#\x0a\x12\x84\x0csS\x1eK\xef\x09\x8e;j\xeb\x9c" +
"f\x01\x93&*\x0b\xdb\xa0\x03q\xfe\xb9\x8b\xd4\xd3m" +
"\x9a\xf6x0B\x95\xe5\xf0\xec\\\xca\xc8\xa0Ufq" +
"\xf2C\xcbt\xa3\x12O\xd8X\x8a'N\x95m\xe3X" +
"\x8a\xf7\x94ws\\g\xe6\x00\x9b\x80\xf8\x10\xcfpk" +
"2.\xe2\xd6\x93\xf6\x8c1^\x99\xb0\xc7]\x93\xf5\xf6" +
"\xae\x83n+\xc32\x01SEA\x1f\x1a.\x17\"\x9c" +
"&\xdb\x84\xb6\xd7\xe3\x8c\x06\x13Fd\xf3=4\xe1^" +
"'o\xb8\x81\x81D\xee\xcd(\xc9\xe5M\x97\xf7X:" +
"\xc47\x98\x86\xceKx8\x93sg\xf9\x8d{\xca\xb9" +
"|\xe7,;\xf7\xbf\xbb\x91\xfe\xf6\"+\x15\xa44\\" +
"\xc8\x89STv)\x9d\xb2-l\x1b\x12\xa7h\xab<" +
"E\x97\xf7\x8b\xb6p\xc9J\x80\xfd\xe9\xbc\xe3\x10i\xca" +
"9\\7\xd2\x1e\x07\xd4\xfd\x9c\xed\xba\xc6\xb0\xc9Kg" +
"\xab\xa5\x8b]\x10\x19a\x86y\xc3\x8b\xf0\xbe\xe2\x95k" +
"\xf9\xc6u\xca5\xf0\x1a\x80\xe4\xa3*&\x9f\x0c4\x16" +
"'\x87\x02w\xc3\x0aJ\xd7\xcf\xec\x0c\xdc\x0d\xab\x8al" +
"\xb0\xce\x1e\x04H>\xabb\xf2B\xe5\x1a\xf8<\xbd\xfd" +
"\x9c\x8a\xc9W\x02\xd7\xc0\xd7I\xf1\x9a\x8a\xc9\xf7\x14\xec" +
"f\"\xbb\xa6\xa4\xabIs\xad\xe4\x8e\xed1s\xbda" +
"\x82\xca]\xac\x01\x05k p}\xcc\xf5\xf5\x86\xc9]" +
"(K\xcc\xe2w+@\xb7tK\xdcP\xfcRP\x12" +
"a\x1f\xf7\x98a\xbaP\xb9F.\xff\xa7\x92\xe25\xf2" +
"l\x88\xc1\xa4,\xb8Q\xf0K\x9f\xd5\xfa\xba\xa5\x0f\x82" +
"\xba\x96\xd7`\x1d5w}*&\x07\x15,-\xc1\x00" +
"E;\xa1b\xf2\xde\xc0\x12l\xa5 nQ1\xf9-" +
"%0\xe5\x00\x15\x9a]\xd4\xfe?\x00\x00\xff\xff\xd6\xbd" +
"\x94\xef"
const schema_b526ba661d550a59 = "x\xda\x94z\x0d\x90T\xd5\x95\xff9\xef\xf5L\xcf'" +
"M\xf3\x1a\x01\x01\xdb!\x928\xc41\xc0\xc8?\x0eh" +
"\x86\xa1g@\xa8\xeea\xde<>\x1c\x94\xaa\xdc\xe9w" +
"\xa7\xe7\xc1\x9b\xf7\x9a\xf7^\xcfLSX \x81*\xf4" +
"\x8f\x15\xcdj\x82)\xd9\xf8\x81\xb5\x9a\x95\x84du\x8b" +
"XX\x0b\x14\xa9B\x17\xaa4\xa5)c\xcc\xfaQ\xd9" +
"\xd2d\xb5\x8cY\xad5\xd9X\xbdu\xee\xed\xaf\xf9\x88" +
"8E\xd5t\xf7\xef\x9cw\xee\xb9\xe7\x9e{\xee\xf9\xdd" +
"\xc7r6\x7fmhE\xf3?\x8d\x81bdkjj" +
"\x0b\xd1\xdd\x03\xef:\xc1O\x0e@t!\x16\x06\x1a\xb6" +
".\x1ez\xfe\xab\xcfA(\x0c\xd0\xfe\x8b5;P\xbb" +
"\xbc&\x0cj\xe1\xa7\x9f$;v\xfc\xe9\xfc\xdd\xd3h" +
"=MZ/\x08\xad\xdb\x13\xe7\xbf;\xf8\xd5\x1f\x1d\x86" +
"\xe8B\xa5\xa2\x05\xd8\xfe\xf0\x9aM\xa8\x9d\\#\xd5\xdd" +
"\x10`\xa1\xed\xc5\xdc\xed?^p\xdf\x03\xa0\xb7a}" +
"\xe1\xbc\xf3o\x8d\xdb.\xdc\xf9\xdfP\x83\xa4\xb3t\xed" +
"\xbd\xa8\xdd\xba\x96\xbev\xac}K\x01,\xfc\xe3\xd7?" +
"\xcaY{w|\x1f\xa2_Q\x0a\x07?\xfd\xf4+\xed" +
"\xff\xf5\xf1\x09\xb2\xbc'q\x10\xb5\xc3\x890\x80vw" +
"\xe2\x07\x80\x859\xcfb&s\xf1\xf2#\xd3x\xfaJ" +
"b\x10\xb5\xffL\x90\xa7m\xbf\x8d\xab\xdd\xe1\xe1\xc7\xa6" +
"\xd1:\x97\xd8\x81\xdakB\xcbV\xdb\xd7~6X\xfb" +
"\x04\xe8\x0b\xb1J\xadF\xe8\xfd\x8c\xf4~\x99\x90\x8fl" +
"G\xc0\x8a_\xfaB\x9cU\xa5^K:\xabz\xaeF" +
"\xad\xa7\xe7k\x00\xed\xacg,\x0cX\xe8N\x9c}\xe9" +
"\xe4[O<9%Z\x7fH\x1dD\xed\xf3\x14\xcd\xe9" +
"/\xa9o\x02\x16\xb6\x9e\xd0\x7fs\xc3\xe8\xe5'\xa7\xf1" +
"\xf6\xf3\xd4\x0e\xd4\xa2\xbd\xe4\xedO~\x9b~\xe8\xff?" +
"\xf5\xd1?O\xb1\xf7aju\xc5\xdef\xc0\xc2\xce\xe7" +
"\xde[\xf1\xdd\xcf\xdf\xfa\xc94\xf6\x9a{w\xa0\xd6\"" +
"\xecu\xbe\xf5\xc7\xbbR\xd7n?5\xed\xa8\x83\xa5Q" +
"/b\xb2\xc5J;\xa7\xa7\xd1\xfa\x90|C\xa15|" +
"\xe6\x1f\x1e4\xd6t\xbf0\x8d\xd6\xef\xc8\xd6\xc7)\xd2" +
":\xa4\xde\xf46\xbf\xe5\xba\xf3\xd3h]&\xadw\x84" +
"\xd6G\xff\xf2\xadWn\xbeu\xd7\xc5\xc9\xab\xa2\x90\xde" +
"\x0b\xa99\xa8]\xa6\xb9\xb6\xbf\x98\x8a\xd3\xaa\xac\x1e\xd8" +
"\x9e\xb5_\xfb\xd1\xbfO7t/\x0d-\x1c|\xf9\xe0" +
"\xa3\x99\xff}\xe3\xfb\x97\xa7\x1b\x9a\xb4\xde\x11Z\x8b/" +
"\xa4\xdf|38\xf8\xca\x94\x10\xbf\xd0\xbb\x0e\xb5\xcb\xbd" +
"b\xd8^\x91\x0c\x1d\x1dg\x1e\xbbt\xef\xff\xfc\x9a\xbc" +
"T'o\xac\xcd\x9bP\xbb\xbc\x99\x16\xe4\xc5\xcd\xef\x01" +
"\x16V%\xbb\xbfsl\xfb\xf7\xdf\x98f\xf4\x93};" +
"P;\xd7G\xa3w|\xe7\xa5\x1f\xfe\xa0\xe6\x07\xffA" +
"&\x95Ij\xc7\xfb\xd6\xa1v\xb2\x8fL>\xdd7\x06" +
"X\xb8\xfe\x95_\x7ftC\xcf\xb3\x7f\x9e\xb2c\x9a\xf5" +
"]\xa8\xb5\xe8\xa4\xb9X\xcf\x00V6\xdfD\xb3r+" +
"n$\xed\x9d\xfa<\x80v\xae\x87)m\x17\x1c?\xbe" +
"\xb1\xfe\x83\xab>\x99\xc6\xd7\x91\x81\x1d\xa8\xdd=@\xbe" +
">\x11\xca~\xbe\xe6\xd0}\x7f\x99Fk'i\xed\x11" +
"Z\xcb\xde_\xda\xfb\xde\x9d\xdf\xfa\xeb\x94x\xa6\x06\x06" +
"Qc\x03\xe4\xe4\xce\x81\xfd\x80\x85\xc7\xfd\xbe\x8b/\xed" +
"|\xe2\xaf\x93\x9d\x14\x8b~|\xe0 j?\x1b\x10\xe1" +
"\x1a\x10\xd1\x8f\xfe\xeb\xd8\x91\x0f\xbb\x06\xff6\xcd\xf0m" +
"w\x0c\xa2\xd6u\x07\x0d\xbf\xff\xd8\xa9\xf7\x8ccG\x0a" +
"\x933I\xcc|\xf1\x1d\xcf\xa3\xb6\xea\x0era\xc5\x1d" +
"?\x85\xc2\xdf\xfd\xf7P!\xcd=\xce\xeco\xa4C9" +
"?pG\xbe\x91\x16\x1f7\xa6Y\xd6\xc9\xaeN\x88\x1f" +
"\xfd\xdc\xe7\xde(W\xcd\x9b\xfa\x10g\xa2\xbf\xfcJ\xfa" +
")\x9657:\xd9\\\xb0%\x9f\xe5\x00}\x88\xfa\x19" +
"T\x00\xb46e\x13\x00\xce\xd2Z\x95\x9f\x03`Dk" +
"U\x1e\x07\xc0\xd9Z\xab\xf2c\x00\x8cj\xad\xcay\x00" +
"\x9c\xa3\xb5*o\x00`\xa3\xd6\xa6\x0c\x02\xa0\xa6\xb5*" +
"/\x01`Lk\x13\x9f\xa8\xadP\xf6\x02\xa0\xa2\xb5)" +
"\xbb\x00p\xae\xd6*~_\xa5-U\xfe\x08\x80\xf3\xb4" +
"V\xe5W\x008_kS\xde\x05\xc0\x05\xda\x0a\xf1\xfb" +
"jm\x95\xf2C\x00\\\xa8\xad\x12\xe3.\xd2V\x09{" +
"\x8b\xb5\x0e\xf1\xfb\x1a\xadC\xf8\xa5\x16\x7f\x87\xb4\x0e\xe1" +
"O\\\xeb\x10v\xaf\xd5n\x15\xf6Z\xda\xbb\x94\xd5\x08" +
"\x805\xdaF\xe5y\x00\xac\xd56\x0a\x07\x9a\xb5\x1ee" +
"\x07\x006i]\xc2\xb1%\xda\xad\xe2\xc1\xafh]\xc2" +
"\xf0u\xc5\xcf\xa5\xed]\xca\x1c2\xd0\xa0mT\xee\x05" +
"\xc0\xafj\x1b\x95?\x03\xe0\xd7\xdaS\xca\x12\x12\\\xaf" +
"m\x15!hm\x1fP\x14\x02\x96\xb5\xef\x94_\xbe\xae" +
"1\xe5{\x00x\x83\xc6\xc4\xa3m\x1a\x13\xc1\x0b\x17\x7f" +
"\xd7iL9\x08\x80\xf5\xdaN\xf1y\xa36@>\x14" +
"Lw\xcc\xb1]f\x02@\xc1\xe7\xc1\x16\xe6e8\x06" +
"I\x16p\x8f\xd9\xf1\xaet\x9a\xdb\x84\x1bY\xceML" +
"Z#V\xb0yh(\xec\xf3`\x12\x9ap\x9dH\xe0" +
"\xb9B9\xc5\xb2\x89\\\x8d7\xca\x85<\xe1:$\x00" +
"\x9f\x07\xdb,\xdfr\x9dDn\x82H\x95\x0f%\xddL" +
"\x92CxT\x8e'4\x15\xa9*|\"\x97\xba\xa4\x8f" +
"\xd5\xb2\x94\xe5H\xf16\x80\x82\xc7i&\x06\x87\xce " +
"\xb0\x9c\x8c_\xf0\xd9(7x\x10@D\xfe\xe4A\x8f" +
"\xc3\x06m\xe8\x94\xc3O6\xb6\xd5\xe7B\xce\x8dHI" +
",\xa6\xa2L\x90e9G\xb3<{E\xcc\xbeJ\x1a" +
".>y\x9bk\x9bI\x85\xf9\x81\xc1\xb9#TI\x13" +
"\x83\xaa(\x0bt\x13W\xbd\xdd\x93\xc1\xaet\xb8\x18x" +
"\x81*\x12\xddb\x8d\xf0\xcdCC>\x0fd \xba\xf9" +
"\x10\xcb\xa1\x1d$\x99\xc3\xb7[a3\x18.\xbb\x8c\xa5" +
"\xb8\xc5E\xe0\x0a\x14\x18R\xc7\x9c\x1dPD\xac0\x05" +
"\x84\xd0~\x9evkFF\xb8crSH\x9c\x8cO" +
"ke\xd8\xeeX\xb7;\xe6\xacw\xbd^>.\x1dH" +
"Fh\xb2\x95\xb9o\xcdN\x90Z\xe1\xa2\x94\xe6n\xa8" +
"\xa59\x07\xdb\x87-\x9b'\x86\x99\x93\xb1\x9c\x8c\xc1;" +
"\xa5\xba\x18\xbd\x8f{>Z~\xc0\x9d\x80\x04r\xd9\xd2" +
"\xccIs\xbb\xdb\x85N\x99\x9b\xc5\xf4\xd8\xe4\x83\xea:" +
"\xc5\x1f\x86\x0b\x91\x9c\x97\xe6bQ\xc7\x03\xee)\x0e\xb3" +
"\xcba\x9e\x90\x8eB\x8c%q<9a\x0e2{\xfb" +
"<+\xeezV\x90/\xe3\xaa4CN\xf3~\xbe'" +
"gy\xdc\xa7\xdd\x90\xc5\xa0\xc0\xe830\xb2X\x1aM" +
".G\x9f\xc7}_\xd9\xc0\xfc-nWQc\xc2x" +
"]\xe6\xae\x9c\xafR\xf8\xe5jVkUb'@%" +
"\xa8L\x85V\xddUsAy\x88z1\xc4\xe6Q\xee" +
"y\x96\xc9+\x8a\xb4j\x09\xd71\xad\xc0r'\x07\xa3" +
"\xb4\x03\x8daf\xbac\x09\xe6\x19\x01\x0bPDO@" +
"8\x96rMno[)\xa7R\xc46d\xfd\xa4\x9b" +
"f\x112H\xf0&\xdfu\xfa\x90\x05\xc3\xebm\x97\x15" +
"g-\xb0Z\x16\x0co\xe1\xe3\xc2\x05\x01\xb0`x\x9d" +
"[=d\xd1\x14Y\xa2\xf5\x88x\x0e\xb3\xcb\x87E\xcd" +
"4\x87\x85\x110\xaf\xcf\xb2\xdd\xa0\xe8,\xbfqC\xd6" +
"\x97\x87\xc6Mj\x08 \x84t\xe0\xe2^\x00\xe3NT" +
"\xd1\x18F\x05\xa3\x881$\x9c\xe3&\x00\xc3$<K" +
"\xb8\xa2\xc4\xc4A3\x82\xfd\x00\x86M\xf88\xe1\xaa\x1a" +
"C\x15@\xcb\x09\xfd\x80\xf0\x03\x84\x87\xeab\x18\x02\xd0" +
"\xee\xc2\x95\x00\xc68\xe1\x87\x08\xaf\xa9\x8fa\x0d\xf5\xef" +
"\xb8\x03\xc08@\xf8Q\xc2k\x1bbX\x0b\xa0\xdd\x83" +
"?\x040\x8e\x12~\x8c\xf0pc\x8c\x0eh\xed!\xbc" +
"\x17\xc08F\xf8\x09\xc2\xeb\x9abX\x07\xa0=&\xf4" +
"O\x10~\x8a\xf0\xfa\xe6\x18\xd6\x03h'\xd1\x030\x9e" +
"!\xfc4\xe1\x0d\xe1\x186\x00h\xcf\xe1\xe3\x00\xc6i" +
"\xc2/\xa0\x82-\x8dJ\x0c\x1b\x01\xb4s\xb8\x1a\xc08" +
"C\xf8ET\x10\x9bb\xd8\x04\xa0\xfd\x12\x97\x01\x18g" +
"\x09~\x13\x15,\xf8.m\x99\x94\x0b\x9d\x8eKi\x85" +
"\xf5\xa0`=`\xc1f\x81\x15\xe4L\x0et\xbc\x82\x82" +
"\x8d\x84\xb9N\x86@@^\xc6\x98=E/\xee\x8bm" +
"\xd4\x00\x0a6\x00\x16\x069\xf3,'\xd3\x0d*\xcf\x94" +
"\xc1a\xd7\xb3\xf6\xbaN\x80\xcc\xeeJ\xa7s\x1eS\xd3" +
"\xf9\xb2p\x94{\x81\x95f6JQ:O\x07\xe0D" +
"k]X\x14uW[\xf5e\xa5LC\\\xc8\xcax" +
"\xce\xb1\xc6\xc5\xec\xfc\x80\x8ddS\x96\x1d\xb6-\x1fk" +
"@\xc1\x1a\xc0\xcea\xe6\xaf\xb7\xc6\x11AA\x04\x8c\x8c" +
"\xf6\xf6t\xe3,\xc0>\x15\x85\x81Y\x80_\x98\x96\xd4" +
"\xc3l\xce\x057\x1al$ksJ\xcc\x9c/\x92r" +
">%X\xb4k\x1d5\"\xd1\x8e\x95\xd4\x87DW\xd0" +
"\xa9\xafF\xdb\x08\x0cE\x97R/P\x13m\xd9\x05\xb0" +
"?\xe7\xecv\xdc1'\xee\xb8\x1b\xb2~\xc1q\x13\xee" +
"(\xf7\x18\xa8\x19\xbe\xdfqS,H\x0f\x17F\xe8/" +
"7{\xa1\xd3\x95%\xa4\x08$AV\xe1/\xdd\x99\x85" +
"\xcd\x15+g\xd8\xcau|\x99V\x8e\xf61\x9d\x1a\x9b" +
"\xd5\\ \"P\xda\x96\xd1\x87\xef\x05\xd0\x1fQQ\x7f" +
"\xaa\xb2'\xa3On\x02\xd0O\xa8\xa8\x9f\xaal\xc8\xe8" +
"\xc9e\x00\xfaS*\xea\x17*\xbb1z\x8e4\xcf\xaa" +
"\xa8_R\x10Cb'F_<\x08\xa0_TQ\x7f" +
"\x95\xb6!\x8am\x18}e\x10@\x7fYE\xfdM\x05" +
"\xcb\x9d\x0c\xf6yn\x86\xea$\xb5\x90\x95f\x1c\x10g" +
"S\xd2\x94\x8e8\xea\xc7@\xc1&\xc0H\x96\x05\xc3\xa5" +
"\x1c\x98]aC\x80\"\x1b\xb2\xae/**\x08{e" +
"bS\xb4g\xbbn\xb6\x9f\x05\x1c\xbbh\x013\xbc\x92" +
"\xbb%\x09\x84S\x96SFKAU\xff^n\xa9\xb9" +
"@\xaf\xc3\xeak\x84\xfa]U\x0c\xa9\xbe\xbfPJ=" +
"\x88P\xf2\x156d}\x83\xf65 \xd7\x8f\x94+\xe3" +
"\xd3\xea:\xaa,*U\x16\xb5\xaa2\x9eT\xa9R<" +
"E\xf8\xb3jUe\xfc\x99J\x15\xf0\x14\xe1gT\x05" +
"\xb1X\x18\x7f\xa1R\xa1;M\xf0\x05R\x0f\xa1,\x8c" +
"\xe7T*\xbcg\x09\xbfDx\x8d\"\x0b\xe3\x8b\xea\xf3" +
"\x00\xc6%\xc2_'\xbcV\x95\x85\xf151\xec\xcb\x84" +
"\xbfIx8$\x0b\xe3o\x84\xfd\xd7\x09\xff=\xe1u" +
"\xaa,\x8c\xef\xa8T\x18\x7fO\xf8\x9f\x08\xaf\x0f\xc9\xc2" +
"\xf8\xa1J\x85\xf1\x03\xc2?#\xbc\xa1F\x16\xc6O\xd5" +
"\xef\x01\x18\x9f\x11\x1e\x0a)\x18m\xac\x95\x85\x11C\xbf" +
"\x020\xeaB*\x1a1\xc2\x9b\xde\x96\x951\x1a\"\x7f" +
"\x9a\x08\x9fOx\xf3\xe2\x186\x03hsCT\xf0g" +
"\x13\xbe\x88\xf0Y\xef\xc4p\x16\x80\xb6 D~\xce'" +
"\xfc:\xc2#u1\x8c\x00h-\xa1\x97\x00\x8c\xeb\x09" +
"\xbf\x89\xf0\xd9\xf51\x9cM4,D\xf1YN\xf8-" +
"\x84G\x1bb\x18\x05\xd0:B4\xaf[\x08\xbf\x8d\xf0" +
"9\x91\x18\xce\x01\xd0zB\x83\x00F7\xe1}\x84k" +
"\x8d1\xd4\x00\xb4T\xe8\xe7\x00F\x1f\xe1w\x12\x1ek" +
"\x8aa\x0c@\x1b\x08\xd1\x81r'\xe1\xc3\x84\xcfm\x8e" +
"\xe1\\:\xf8B\x14\x1f\x93\xf0,\xe1W-\x8a\xe1U" +
"t\xf0\x09\xfd,\xe1\xfb\x08\x9f\xf7n\x0c\xe7\x01hy" +
"\xe1\xcf>\xc2\x8f\x10>\x7fq\x0c\xe7\x03h\x87C\xbb" +
"\x00\x8cC\x84\xdfO\xf8\x82\xba\x18.\x00\xd0\xee\x13\xf1" +
"9B\xf8\x83\x84_]\x13\xc3\xab\x01\xb4\x07\x84\x9f\x0f" +
"\x12\xfe(\xe1\x0b\xaf\x89\xe1B\x00\xed\xb8\xf0\xe7\x11\xc2" +
"\x9f\"|Q<\x86\x8b\x00\xb4'E<O\x10~\x8a" +
"\xf0\xc5\xd7\xc5p1\xa5g\x88\x0e\xe8g\x08?M\xf8" +
"5\x0d1\xbc\x86\x0e>\x11\x9fg\x09?Kx\xbc1" +
"\x86q\x00\xed\x051\xaf\xb3\x84_\"\xfc\xda\xa6\x18^" +
"Ky(\xf0K\x84\xbfNxKs\x0c[(\x0fC" +
"\x07\x01\x8cW\x09\x7f;\xa4\xe0\xfe1\x96\xefe#\xbc" +
"T\x0a:\xc7X\xbe\x9f\x0f\x95~\x16<\x97\x99$\xaf" +
"\xaa\x16\xf2\xd8IZ#\xa0ZAy[;\xc5&\x18" +
":e\xcb5E\x80\x12\xef\xb6:\xfd\x80\xda\xdb\x92B" +
"\xe70\xdb\xcb<\xb3l\x9d\xf4oc{\x19\xa8\xd3\x80" +
"\xe8\x99\xdd\x16=\xafV\x0c\x14\x989j\xf9\xae\x97\x87" +
"\xb81\xe1$\xa6\x87\xba\xccQ\x0bI(]\x98\"S" +
"J\xb2\xa2\xdd4V\x1cs\x1d\xbe\x9d\xe5K\x07e\xdc" +
"f\x0e\xf7\xb1\x16\x14\xac\x05,\x04\x96\xcd\x93D\x1bU" +
"n\x96T\xca\x91Q\xac\xc0\xc8e2\xdc\x0f\xb8)\x8c" +
"W\x9d\xe8~Q\x00\x9d\xe6Dw\xb9\x1fX#TH" +
"\xcd~\x97\x99\xdb-S\x0d\x86\xcbBZ\x07\xeab!" +
"\xcc\xc7\x03\x8cT\xee\x0f\x011\x02X0\xadbT\xd7" +
"{\xee\xc8v\x96O\xc4\xb9CM\x7f\xb9\xc7(\x12O" +
",1\xcf*\x8fF\x88/y\xa3|r\xfc\xc6X\xde" +
"\xe06O#\x15\x7fyc\x81\x91\xca5Pq\xe4\xd2" +
"\x9c\xd1\x92\x9d{P\x1d\x90a+3<\xc6\xf2\x09\x88" +
"\xd8\xcc\xf71R\xb9\x94\x93O\xc7\xc7X~\xa3Yj" +
"M\x0a\xe9b\xf3>\x89\xabT2O\x14\xfem\x1c\xe2" +
"\x1eM\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd1 \xe5" +
"\x93\xa3\xe8b\xa6r@`\xa4r\x94T\xa4)\xd7q" +
"\xb7@\xb8\xba!L\xbb#\xd9\\\xc0M\x142\x8bv" +
"@Y&\xbb\xc8\x0d\x1c\x1d\xee1y2\x96eY\xcf" +
"5si\xee\xa1\xc1}\xf2\xb1\"\xf9\xc2^\xbfLU" +
"$\xbd\x15\xd1\xa6\x96\xa2N4\x09\xd1\xd5\xa2\xa9\xaa_" +
"\x07@\xbb'\xb0\xd2\xfb\xb3\xdcKs'\x98IO\xb3" +
"\xaa\xba\xa7\x99\xee\xf8\xa5\xbcKt\xbaN\xc0\xc7E?" +
"\xd3$\x06_,;\xba\xb9\xcbDG\xd7\xbc\x0e`\xff" +
"\x90\xc7\xf9\x18\xcbG\xd2V\x90/ur3\xf1d\xc5" +
"\x8c\xba\xb1\xb0\xb9b\xa67q\xdf\x9c\xe9\x00\xabf\xfa" +
"\xc0\x8a+\x05S\xdc\xf5\xa1Cq\x8c\x95\xfb\xc2\xbb\xa8" +
"\xdb\x1bWQ?T\xd5\x17\xde\xbd\x12@\xdf\xa7\xa2~" +
"DA,\xb6\x85\x87\x97\x00\xe8\x07T\xd4\x8fR[8" +
"[\xb6\x85\xf7\xd0\xd3\x87T\xd4\xef\xaf4\"\xd1\xfb\xa8" +
"W<\xaa\xa2~L\xc1H\x90\xcfR\x8a\x97\xdf\xed\x14" +
"w\xd9\x10\xb1\xd5\xd2\xbe\x0e\xfb\x81W\xee\xfc\x06]\xd7" +
".\xef\xd6]E\xe2Z]\xecg\x12\x93\xe53\x0db" +
"\xfb\x95\x1e\xb8\xadX?\xa8z\x88\xfdp\xb3\x88\xcec" +
"2%\x1f\xde$R\xf2\xa1]\x82d<\xb0R\x90\x8c" +
"{\xfa\x05\xc98L*\xb5\xd1\xbb\x06\x010\x1c\xcd\x13" +
"X\x17\xcdy\xb4S\xa3{\xe8\xb9\x86\xe8\x08=\xd7\x18" +
"\xb5\xe8\xa3)\xcaI\xb39\xca\xaa\xc8Ia\xc4\x0d\\" +
"o\x8c\xe5\x01\xa0\xf2=\x92\xb4\x9c\xdd\xf1\xc0\xcb9\xbb" +
"\x0b\xe2o\xd2r\x00w\xef\xcfz\xd6\x08\xf3\xf2\x85\xe2" +
"g\x12\xc2\x96\xb3\xbb\xe0s*j\xcc\x03\xccW\xbe\xc7" +
"\xf3d\xa3\x10\x10\xf1c\x9e0_\xfe.\xcc\x17rN" +
"\x9a\xe6lAd\xc8\xe2f\xc1\xe3\xbeer*\xff\x16" +
"\xb3\x0b\xb65j9\x19\xea\x7f=\xce\x83+\xd2\x14Z" +
"\xd2>\xd7r\x02yy0\xbb\x9c\x8d\x8cR\xe7\xdb*" +
"\xeavU6Z\xfd\x00\xfa\xb0\x8az@\xcdqH\xa6" +
"\xe3\x1e\x02\xb3*\xea\xfb(\x1dkd:\xe6\xf7V\x92" +
"\xf9K\x13\xe8t\xce\x1beA\xce\x83\xcaA[\x08\xe4" +
"\x05&\x87N\xdb\xa5\x922\x85+|\x89\x1d\xdf\xfee" +
"\x08[_\x89\xc9\x88\xd2Z\x8eC+\xc5\xe1z\x15\xf5" +
"\x9b\xaa\xe2\xb0\x82\xa6\xbc\\E\xfd\x96\x99\xcc\xee\x0a\x0e" +
"\x10q\x16tE\\\xdd\x89\xd5\x10\xf1m]&\x12\xba" +
"E&\xf4\xe2M\"\xa1\x17,\x01\x888\xae\xc3\x0b\x16" +
"\x9d\xe5\x0e\xb3)Q\xf8x\xf9{8\xcd\xbc/\x1cr" +
"\xc2\x15R$`\x01\xd7CX\xf5>7\x8aK\xc2\x1b" +
"\xb2\xbe~\xa0\xcc\x9a\xf6\xa8\xd4\xf6f\x89F\xec\xabf" +
"MyAk\xf6\x11~\xa4\x9a5\x1dV\xa9-=D" +
"\xf8\xfd\x84\xab(i\xd3}\xea\xe3\x00\xc6\xfd\x84?\"" +
"h\x93*i\xd3\xc3\xc2\xfe1\xc2O\x08\xda\x14\x92\xb4" +
"\xe91\x95\xda\xd2G\x09\x7fF\xd0\xa6\x1aI\x9b\x9e\x16" +
"v\x9e!\xfc\xb4\xa0M\xb5\x926=\xa7\x9e\x9f@\xcb" +
"\xea\xc2\x926\x9d\x13\xac\xef\x0c\xe1\x17\x05m\xaa\x93\xb4" +
"\xe9\x97\x82f] \xfceA\x9b\xea%m\xba,X" +
"_\x85\xae56H\xda\xf4\x9a\xf0\xf3U\xc2\xdf&\xbc" +
"\xa9Q\xd2\xa6\xdf\x09\x9a\xf56\xe1\x1f\x10\xde\xdc$i" +
"\xd3\x1fTj\xdb\xdf'\xfc\x13\xc2g5K\xda\xf4\xb1" +
"\xa0q\x9f\xa8*\xf6\x0b\xd64K\xb2\xa6\xcf\xc5\xb0\x7f" +
"#\xf5:\xc1\x9a\"\x925\xd5\x88.?D\xdd\xf9l" +
"\xc1\x9afK\xd6\xd4,XD\x85\xad\xcd\x89J\xd64" +
"7\xf4\xfc\x04V\xa6\xa9\x925\xb5\x84\xc8\xcd\xeb\x08_" +
".X\xd3\x1c\xc9\x9a\xda\x84\x9d\x1b\x08\xbfY\xb0&M" +
"\xb2\xa6UB\xfff\xc2\xbb\x05k\x8aI\xd6\xd4%X" +
"M\x85\x95\xcd\x9b+YSJ\xd8I\x12~\xbb`M" +
"WI\xd6\xb4U\xb0\x8b-\x84\x7f[\xb0\xa6y\x925" +
"\xed\x14l\xb0\xc2\xd6\xae^+Y\x13\x17\xe3\x0e\x13\x1e" +
"\x08\xd6\xd4%Y\xd3\x1ea\xa7\xc2\xd6\x16\xad\x93\xac)" +
"/\xf0q\xc2\x0f\x09\xd6\x94\x90\xac\xe9n\x81\x1f \xfc" +
"\xa8`M\xdd\x925\xdd#\xfc,\xb35\x8cK\xd2\xf4" +
"@h\x09\x80q\x94\xe0c!\x05\xc5}\xb6\xdd\xe7q" +
"\x88\xf8~U\xf7\xca\xec1\x96\xf77;X|\x1b\xd4" +
"\xd3)^qT\x14\x06=\xb6\x9b'\xad\x0c\x84\x87\x03" +
"\xbf\x8c\x9a\xcc\x1f\x1et\x99\x87f\xd5\xcdv\xb9\xa6\x99" +
"|\xda\xb1\xca\x0d\xbc\xb8\xe1\xe6&L\x95$]'C" +
"\xd2\xf0t\xcf)\xdb\xb8\x97/iT?\xbf\x9f\xa7\xdd" +
"\x0d\x9cy\xe5'\x86\\/\xcd\x13.\x03\xd5\x0f\xca\xa0" +
"\xe5\xf71o7\xd1\x82\xca\xc0Y\x96\xf3y\x92\x05\x10" +
"\xa1\xc9O\x82]\x07E\xf5\xb3\x1c\x86v\x15\x01r\xbd" +
"`\x03g\x80\x95\xf1\x02\x8f\x0d\x0dY\xe9\x14\xba\xa6|" +
"GTM\x102\xcc7\x02\x96\xde]=\xee\x88kr" +
"\x9a\x05L\x98\xa8,l}\x1e\xc4\xf9\x17.RW\xa7" +
"m\xbbc\xd5\x11\xaa,G\xe0f\x0d+\x83N\x99\xc5" +
"\xc9\x17-S\x8dJ<\xe9b)\x9e8Y\xb6\x8dc" +
")\xde\x93\x9e\xcdr\x93\xd9)6\x0e\xf1~\x9e\xe1\xce" +
"D\\\xc4\xad+\x1dX\xa3\xbc2\xe1\x80\xfb6K$" +
"z\xa0\xd3\xc9\xb0L\x95\xa9\xa2\xa0\x1b-\x9f\x0b\x11N" +
"\x91\xf5\xa2\x1bty#\xd5\x09#\xb2\xf96\x9ap\xc2" +
"\xcbY~\xd5@\"\xf7\xa6\x95ds\xb6\xcf\xbb\x1c\x13" +
"\xe2\x1bl\xcb\xe4%<\x9c\xc9\xfa8\xbbr`\x14\xef" +
"\xfcfp.\xdf<\xc3\xce\xfd\xff]I\x7f{\x91\x95" +
"\x0aR\x1a\xceg\xc5)*\xbb\x94U\xb2-l\xeb\x17" +
"\xa7h\xab<E\x97n\x12ma\xcb2\x80\xfd\xe9\x9c" +
"\xe7\x11i\xcaz\xdc\xb4\xd2\x01\x074\x0bY\xd7\xf7\xad" +
"A\x9b\x97\xceV\xc7\x14\xbb 2\xc4,\xfb\x8a\x17\xe1" +
"\xdd\xc5+\xd7\xf2\x8d\xeb\xa4k\xe0\xd5\x00\xfa\x83*\xea" +
"\x8fV5\x16\xc7\xfb\xab\xee\x86\x15\x94\xae?\xb9\xa3\xea" +
"nXUd\x83u\xf2 \x80\xfe\x8c\x8a\xfa\xe9\xca5" +
"\xf0s\xf4\xf4\xb3*\xea/W]\x03_&\xc5K*" +
"\xea\xef+\xd8\xc9DvMJW\x9b\xe6Z\xc9\x1d7" +
"`\xf6z\xcb\x06\x95\xfbX\x07\x0a\xd6A\xd5\xf517" +
"\xd7[6\xf7\xa1,\xb1\x8b\xef\xad\x00\xfd\xd2-qS" +
"\xf1MAI\x84\xdd<`\x96\xedC\xe5\x1a\xb9\xfc\x1f" +
"6\x8a\xd7\xc83!\x06\x13\xb2\xe0J\xc1/\xbdV\xeb" +
"\xee\x94>\x08\xeaZ^\x83\x1ej\xee\xbaU\xd4\xfb\x14" +
",-A\x8a\xa2\x9dTQ\xbf\xbdj\x09\xb6R\x10\xb7" +
"\xa8\xa8\x7f[\xa9\x9ar\x15\x15\x9aY\xd4\xfe/\x00\x00" +
"\xff\xff\x00\xd1\x99X"
func RegisterSchema(reg *schemas.Registry) {
reg.Register(&schemas.Schema{
+13 -113
View File
@@ -1,11 +1,8 @@
package cereal
import (
"encoding/json"
"log/slog"
"math"
"os"
"regexp"
"capnproto.org/go/capnp/v3"
"pfeifer.dev/mapd/cereal/custom"
@@ -18,8 +15,6 @@ const (
externalFixMaxAge = 500_000_000 // 10 Hz source: 500 ms
internalFixMaxAge = 2_000_000_000 // 1 Hz source: 2 s
carFixMaxAge = 2_500_000_000 // Optional CAN observation: 2.5 s
phoneFixMaxAge = 10_000_000_000 // Companion app in the background sends sparsely: 10 s
gpsModeRefresh = 1_000_000_000 // Galaxy navigation setting reread interval
maxHorizontalAccuracy = 50.0 // input sanity ceiling, not road-match qualification
maxClockPairSkew = 1_000_000 // an interrupted pair cannot date a source event
maxClockOffsetDrift = 1_000_000 // a larger BOOTTIME/MONOTONIC step fences queued fixes
@@ -44,53 +39,8 @@ const (
GpsSourceInternal
GpsSourceExternal
GpsSourceCar
GpsSourcePhone
)
// Galaxy navigation's GPS source setting: backup uses the phone only without
// comma or car GPS, comma never uses it, phone prefers it whenever it has a fix.
const (
GpsModeBackup = "backup"
GpsModeComma = "comma"
GpsModePhone = "phone"
)
var storagePrefix = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
func navigationSettingsPath() string {
prefix := os.Getenv("OPENPILOT_PREFIX")
if prefix == "" {
return "/data/starpilot/navigation/settings.json"
}
if !storagePrefix.MatchString(prefix) {
return ""
}
return "/data/starpilot-" + prefix + "/navigation/settings.json"
}
// ReadGpsMode returns the saved choice, or backup when it is missing or invalid.
func ReadGpsMode() string {
path := navigationSettingsPath()
if path == "" {
return GpsModeBackup
}
raw, err := os.ReadFile(path)
if err != nil || len(raw) > 1<<20 {
return GpsModeBackup
}
var settings struct {
GpsSource string `json:"gpsSource"`
}
if json.Unmarshal(raw, &settings) != nil {
return GpsModeBackup
}
switch settings.GpsSource {
case GpsModeComma, GpsModePhone:
return settings.GpsSource
}
return GpsModeBackup
}
type GpsSample struct {
Location log.GpsLocationData
Source GpsSource
@@ -110,12 +60,7 @@ type GpsSub struct {
gpsLocation Subscriber[log.GpsLocationData]
gpsLocationExternal Subscriber[log.GpsLocationData]
carGps *Subscriber[custom.StarPilotCarState_Gps]
phoneGps *Subscriber[custom.StarPilotCarState_Gps]
car gpsCandidate
phone gpsCandidate
mode func() string
modeValue string
modeReadAt uint64
internal gpsCandidate
external gpsCandidate
selected GpsSource
@@ -138,8 +83,6 @@ func GpsFixFreshAt(source GpsSource, fixMonoTime, now uint64) bool {
return now-fixMonoTime <= externalFixMaxAge
case GpsSourceCar:
return now-fixMonoTime <= carFixMaxAge
case GpsSourcePhone:
return now-fixMonoTime <= phoneFixMaxAge
case GpsSourceInternal:
return now-fixMonoTime <= internalFixMaxAge
default:
@@ -191,35 +134,6 @@ func (s *GpsSub) clearCandidates() {
s.internal = gpsCandidate{}
s.external = gpsCandidate{}
s.car = gpsCandidate{}
s.phone = gpsCandidate{}
}
func (s *GpsSub) gpsMode(now uint64) string {
if s.modeValue == "" || now < s.modeReadAt || now-s.modeReadAt >= gpsModeRefresh {
read := s.mode
if read == nil {
read = ReadGpsMode
}
s.modeValue, s.modeReadAt = read(), now
}
return s.modeValue
}
// updateWrapped admits a StarPilotCarState.Gps-shaped fix dated by its own source time.
func (c *gpsCandidate) updateWrapped(event DecodedEvent[custom.StarPilotCarState_Gps], sensor log.GpsLocationData_SensorSource,
clock clockSample, barrierMono, maxAge uint64) bool {
sourceTime := event.Value.SourceMonoTime()
if sourceTime == 0 || sourceTime > event.LogMonoTime || event.LogMonoTime > clock.monoAfter {
return false
}
location, err := carGpsLocation(event.Value)
if err != nil {
return false
}
location.SetSource(sensor)
valid := event.Valid && !math.IsNaN(float64(location.Speed())) && !math.IsInf(float64(location.Speed()), 0) && location.Speed() >= 0 &&
!math.IsNaN(location.Altitude()) && !math.IsInf(location.Altitude(), 0)
return c.updateWithAge(DecodedEvent[log.GpsLocationData]{Value: location, Valid: valid, LogMonoTime: sourceTime}, clock, barrierMono, maxAge)
}
func (s *GpsSub) clockReady(clock clockSample) bool {
@@ -252,7 +166,7 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
clock = s.clock()
}
ready := s.clockReady(clock)
internalNew, externalNew, carNew, phoneNew := false, false, false, false
internalNew, externalNew, carNew := false, false, false
if event, ok := s.gpsLocation.ReadEvent(); ok && ready {
internalNew = s.internal.update(event, clock, s.barrierMono)
}
@@ -262,29 +176,23 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
if s.carGps != nil {
if event, ok := s.carGps.ReadEvent(); ok && ready {
carNew = s.car.updateWrapped(event, log.GpsLocationData_SensorSource_car, clock, s.barrierMono, carFixMaxAge)
}
}
if s.phoneGps != nil {
if event, ok := s.phoneGps.ReadEvent(); ok && ready {
// The enum has no phone value; android is the closest.
phoneNew = s.phone.updateWrapped(event, log.GpsLocationData_SensorSource_android, clock, s.barrierMono, phoneFixMaxAge)
sourceTime := event.Value.SourceMonoTime()
if sourceTime > 0 && sourceTime <= event.LogMonoTime && event.LogMonoTime <= clock.monoAfter {
location, err := carGpsLocation(event.Value)
if err == nil {
carNew = s.car.updateWithAge(DecodedEvent[log.GpsLocationData]{Value: location, Valid: event.Valid && !math.IsNaN(float64(location.Speed())) && !math.IsInf(float64(location.Speed()), 0) && location.Speed() >= 0 && !math.IsNaN(location.Altitude()) && !math.IsInf(location.Altitude(), 0), LogMonoTime: sourceTime}, clock, s.barrierMono, carFixMaxAge)
}
}
}
}
mode := s.gpsMode(clock.boot)
phoneFresh := ready && mode != GpsModeComma && GpsFixFreshAt(GpsSourcePhone, s.phone.fixMonoTime, clock.boot)
source := GpsSourceNone
if phoneFresh && mode == GpsModePhone {
source = GpsSourcePhone
} else if ready && GpsFixFreshAt(GpsSourceExternal, s.external.fixMonoTime, clock.boot) {
if ready && GpsFixFreshAt(GpsSourceExternal, s.external.fixMonoTime, clock.boot) {
source = GpsSourceExternal
} else if ready && GpsFixFreshAt(GpsSourceInternal, s.internal.fixMonoTime, clock.boot) {
source = GpsSourceInternal
} else if ready && GpsFixFreshAt(GpsSourceCar, s.car.fixMonoTime, clock.boot) {
source = GpsSourceCar
} else if phoneFresh {
source = GpsSourcePhone
}
sample.SourceChanged = source != s.selected
if sample.SourceChanged {
@@ -307,10 +215,6 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
sample.Location = s.car.location
sample.FixMonoTime = s.car.fixMonoTime
sample.NewFix = carNew
case GpsSourcePhone:
sample.Location = s.phone.location
sample.FixMonoTime = s.phone.fixMonoTime
sample.NewFix = phoneNew
default:
return sample, false
}
@@ -330,28 +234,24 @@ func (s *GpsSub) Close() {
if s.carGps != nil {
s.carGps.Sub.Msgq.Close()
}
if s.phoneGps != nil {
s.phoneGps.Sub.Msgq.Close()
}
}
func GetGpsSub() (gpsSub GpsSub) {
return GpsSub{
carGps: optionalWrappedGpsSubscriber("starpilotCarState", CarGpsReader),
phoneGps: optionalWrappedGpsSubscriber("starpilotNavigation", PhoneGpsReader),
carGps: optionalCarGpsSubscriber(),
gpsLocation: NewSubscriber("gpsLocation", GpsLocationReader, true, ms.Settings.SubscriberSettings.ShadowGpsLocation),
gpsLocationExternal: NewSubscriber("gpsLocationExternal", GpsLocationExternalReader, true, ms.Settings.SubscriberSettings.ShadowGpsLocationExternal),
}
}
func optionalWrappedGpsSubscriber(service string, reader func(log.Event) (custom.StarPilotCarState_Gps, error)) (subscriber *Subscriber[custom.StarPilotCarState_Gps]) {
func optionalCarGpsSubscriber() (subscriber *Subscriber[custom.StarPilotCarState_Gps]) {
defer func() {
if recover() != nil {
subscriber = nil
slog.Warn("Optional GPS subscriber unavailable", "service", service)
slog.Warn("Optional car GPS subscriber unavailable")
}
}()
value := NewSubscriber(service, reader, true, false)
value := NewSubscriber("starpilotCarState", CarGpsReader, true, false)
return &value
}
+1 -1
View File
@@ -2623,7 +2623,7 @@ struct Event {
# DO change the name of the field and struct
# DON'T change the ID (e.g. @107)
# DON'T change which struct it points to
starpilotNavigation @107 :Custom.StarPilotCarControl;
customReserved0 @107 :Custom.CustomReserved0;
customReserved1 @108 :Custom.CustomReserved1;
starpilotCarState @109 :Custom.StarPilotCarState;
customReserved3 @110 :Custom.CustomReserved3;
File diff suppressed because it is too large Load Diff
-125
View File
@@ -1,125 +0,0 @@
package cereal
import (
"capnproto.org/go/capnp/v3"
"pfeifer.dev/mapd/cereal/custom"
"pfeifer.dev/mapd/cereal/log"
"testing"
)
func phoneGpsWire(t *testing.T, source, envelope uint64) []byte {
t.Helper()
message, segment, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
t.Fatal(err)
}
event, err := log.NewRootEvent(segment)
if err != nil {
t.Fatal(err)
}
event.SetLogMonoTime(envelope)
event.SetValid(true)
navigation, err := event.NewStarpilotNavigation()
if err != nil {
t.Fatal(err)
}
gps, err := navigation.NewPhoneGps()
if err != nil {
t.Fatal(err)
}
gps.SetSourceMonoTime(source)
gps.SetHasFix(true)
gps.SetLatitude(41)
gps.SetLongitude(-111)
gps.SetAltitude(100)
gps.SetSpeed(12)
gps.SetBearingDeg(90)
gps.SetHorizontalAccuracy(5)
gps.SetVerticalAccuracy(5)
gps.SetBearingAccuracyDeg(10)
gps.SetSpeedAccuracy(1)
raw, err := message.Marshal()
if err != nil {
t.Fatal(err)
}
return raw
}
func attachPhone(s *GpsSub, mode *string) *[][]byte {
queue := [][]byte{}
s.phoneGps = &Subscriber[custom.StarPilotCarState_Gps]{reader: PhoneGpsReader, maxBytes: 250 * 1024, readBytes: func() []byte {
if len(queue) == 0 {
return nil
}
value := queue[0]
queue = queue[1:]
return value
}}
s.mode = func() string { return *mode }
return &queue
}
func TestPhoneGpsFollowsTheGalaxySetting(t *testing.T) {
now := uint64(10_000_000_000)
mode := GpsModeBackup
s, internal, _ := testGpsSub(&now)
car := attachCar(s)
phone := attachPhone(s, &mode)
*phone = append(*phone, phoneGpsWire(t, now-300_000_000, now))
sample, ok := s.ReadSample()
if !ok || sample.Source != GpsSourcePhone || sample.FixMonoTime != now-300_000_000 || sample.Location.Longitude() != -111 {
t.Fatalf("backup without comma or car GPS: %+v %v", sample, ok)
}
*car = append(*car, carGpsWire(t, now-1, now, true, true))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceCar {
t.Fatal("car ranks above the phone in backup")
}
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
t.Fatal("comma ranks above the phone in backup")
}
mode = GpsModePhone
now += gpsModeRefresh
*phone = append(*phone, phoneGpsWire(t, now-100_000_000, now))
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourcePhone {
t.Fatal("prefer phone")
}
now += phoneFixMaxAge
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
t.Fatal("prefer phone falls back to comma once the phone fix expires")
}
mode = GpsModeComma
now += gpsModeRefresh
*phone = append(*phone, phoneGpsWire(t, now, now))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
t.Fatal("comma and car only keeps comma GPS")
}
now += internalFixMaxAge + 1
*phone = append(*phone, phoneGpsWire(t, now, now))
if _, ok = s.ReadSample(); ok {
t.Fatal("comma and car only must never use the phone")
}
}
func TestPhoneGpsModeIsRereadAtMostOnceASecond(t *testing.T) {
now := uint64(10_000_000_000)
reads := 0
s, _, _ := testGpsSub(&now)
s.mode = func() string { reads++; return GpsModeBackup }
s.ReadSample()
now += gpsModeRefresh - 1
s.ReadSample()
if reads != 1 {
t.Fatalf("mode reread early: %d", reads)
}
now++
s.ReadSample()
if reads != 2 {
t.Fatalf("mode not reread: %d", reads)
}
}
-8
View File
@@ -45,11 +45,3 @@ func CarGpsReader(evt log.Event) (custom.StarPilotCarState_Gps, error) {
}
return state.Gps()
}
func PhoneGpsReader(evt log.Event) (custom.StarPilotCarState_Gps, error) {
navigation, err := evt.StarpilotNavigation()
if err != nil {
return custom.StarPilotCarState_Gps{}, err
}
return navigation.PhoneGps()
}
-1
View File
@@ -61,7 +61,6 @@ var ServiceQueueSize = map[string]int64{
"selfdriveState": QUEUE_SIZE_SMALL,
"gpsLocation": QUEUE_SIZE_SMALL,
"starpilotCarState": QUEUE_SIZE_SMALL,
"starpilotNavigation": QUEUE_SIZE_MEDIUM,
"gpsLocationExternal": QUEUE_SIZE_SMALL,
"liveLocationKalman": QUEUE_SIZE_SMALL,
"gpsNMEA": QUEUE_SIZE_SMALL,
+1 -1
View File
@@ -176,7 +176,7 @@ func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader
pos := m.PosFromLocation(location)
s.Position = pos
// Unknown CAN direction or speed uncertainty cannot create confident road evidence.
if (sample.Source == cereal.GpsSourceCar || sample.Source == cereal.GpsSourcePhone) && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
if sample.Source == cereal.GpsSourceCar && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
s.clearRoadDerived()
s.roadFixMonoTime = 0
s.RoadStatus = custom.MapdOut_SampleStatus_noMatch
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,90 +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.
if not self.pending.active or not 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000:
command = Command()
self.announced = 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]
@@ -38,8 +38,6 @@ class Inputs:
lane_direction: int = 0
limit_status: int = 0
geometry: LaneGeometry | None = None
speed_min: float | None = None
speed_max: float | None = None
@dataclass(frozen=True)
@@ -51,9 +49,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
@@ -88,12 +85,6 @@ class AngleController:
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
self.reset()
return Command()
speed_min = max(i.speed, 0.1) if i.speed_min is None else i.speed_min
speed_max = max(i.speed, 0.1) if i.speed_max is None else i.speed_max
if (not all(math.isfinite(v) for v in (speed_min, speed_max)) or
not 0 < speed_min <= max(i.speed, 0.1) <= speed_max):
self.reset()
return Command()
if i.steering_pressed and not self.press_last:
self.preturned = abs(i.steering_angle) > 45.0
self.press_last = i.steering_pressed
@@ -108,7 +99,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:
@@ -128,7 +119,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
@@ -161,7 +152,7 @@ class AngleController:
# measured-curvature interval and the modern ISO envelope. If those bands
# cannot meet the previous actuator ROC, send a neutral mode-0 frame.
effective_speed = max(i.speed, 0.1)
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(speed_max, 1.0) ** 2)
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(i.speed, 1.0) ** 2)
low_kappa, high_kappa = -cap, cap
if i.speed > 9.0:
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
@@ -170,10 +161,8 @@ class AngleController:
# gain (0.95..1.3), so the worst equivalent curvature remains bounded.
lower_gain = bounds.GAIN_MAX if low_kappa >= 0.0 else bounds.GAIN_MIN
upper_gain = bounds.GAIN_MIN if high_kappa >= 0.0 else bounds.GAIN_MAX
lower_speed = speed_max if low_kappa >= 0.0 else speed_min
upper_speed = speed_min if high_kappa >= 0.0 else speed_max
lower = max(-bounds.PATH_MAX, low_kappa * lower_speed * lower_gain, self.path_last - roc)
upper = min(-bounds.PATH_MIN, high_kappa * upper_speed * upper_gain, self.path_last + roc)
lower = max(-bounds.PATH_MAX, low_kappa * effective_speed * lower_gain, self.path_last - roc)
upper = min(-bounds.PATH_MIN, high_kappa * effective_speed * upper_gain, self.path_last + roc)
# Bound the actual-angle equivalent derivative at the worst admitted gain.
# 45ms core margin remains inside the native elapsed-time/50ms cap.
jerk_delta = bounds.LATERAL_JERK / max(i.speed - 1.0, 1.0) ** 2 * bounds.CORE_STEER_PERIOD_S * bounds.GAIN_MIN * effective_speed
@@ -195,7 +184,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:
@@ -1,7 +1,7 @@
"""StarPilot's CP adapter; model transport and saved Params stay in the host.
Only exact final CP words derived from the existing five classic/six CANFD
extended profiles can select angle. Combined AOL retains its original Ford profile identity.
extended profiles can select angle. AOL and Transit keep their current owner.
"""
from opendbc.car import structs
from opendbc.car.ford.values import CAR, FordFlags
@@ -18,7 +18,7 @@ TRUCKS = CANFD - {CAR.FORD_ESCAPE_MK4_5, CAR.FORD_MUSTANG_MACH_E_MK1}
def mode_word(cp, *, selected):
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience != 0 or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
not cp.pcmCruise or len(cp.safetyConfigs) not in (1, 2)):
return None
@@ -32,13 +32,7 @@ def mode_word(cp, *, selected):
safety = cp.safetyConfigs[-1]
long = int(cp.openpilotLongitudinalControl)
original = (18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else 66) + long if fd else 32 + long
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
return None
if cp.alternativeExperience == 32:
expected = original | (256 if manual else 0) | (ANGLE_FLAG if selected else 0)
else:
expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original
expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original
return expected if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected else None
@@ -55,10 +49,7 @@ def select(cp, raw):
chosen = (type(raw) is int and raw == 1) or (type(raw) in (bytes, str) and raw in (b"1", "1"))
if not chosen or mode_word(cp, selected=False) is None:
return False
if cp.alternativeExperience == 32:
cp.safetyConfigs[-1].safetyParam |= ANGLE_FLAG
else:
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
cp.steerActuatorDelay = 0.22
return True
@@ -88,37 +79,16 @@ class Host:
self.cp = cp
self.controller = AngleController(low_gain=low, high_gain=high)
self.inputs = None
self.steering_rejected = False
def update(self, CC, CS):
if self.inputs is not None:
self.inputs.update()
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
not CS.out.canTimeout and
(not aol or CS.out.gearShifter in (structs.CarState.GearShifter.drive,
structs.CarState.GearShifter.sport, structs.CarState.GearShifter.low)) and
CC.enabled and CC.latActive and CS.out.cruiseState.enabled and CS.out.canValid and
not CS.out.canTimeout and not CS.out.gasPressed and not CS.out.brakePressed and
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
speed_min = speed_max = None
speed_window = getattr(CS, "bluepilot_speed_window", None)
if speed_window is not None:
low, high, step = speed_window
# Allow source progress across the existing 100 ms CAN freshness window.
margin = max(0.05, 5 * step)
speed_min = max(0.1, min(low, CS.out.vEgoRaw) - margin)
speed_max = max(high, CS.out.vEgoRaw, 0.1) + margin
meta = model.meta if model is not None else None
return self.controller.step(Inputs(
active=ready, speed=CS.out.vEgoRaw, measured_curvature=-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1),
@@ -128,4 +98,4 @@ class Host:
lane_change=int(getattr(meta.laneChangeState, "raw", meta.laneChangeState)) in (1, 2, 3) if meta else False,
lane_direction=int(getattr(meta.laneChangeDirection, "raw", meta.laneChangeDirection)) if meta else 0,
limit_status=getattr(CS, "lat_ctl_lim_stat", 0),
geometry=model_geometry(model, fresh=snapshot is not None), speed_min=speed_min, speed_max=speed_max))
geometry=model_geometry(model, fresh=snapshot is not None)))
@@ -41,9 +41,6 @@ static bool ford_bp_configure(uint16_t param) {
#ifdef ALLOW_DEBUG
ford_bp_enabled |= (param == 131U) && (alternative_experience == 0);
#endif
const bool joint = ((unsigned int)alternative_experience == 32U) &&
((param == 146U) || (param == 147U) || (param == 160U) || (param == 161U) || (param == 194U) || (param == 195U));
ford_bp_enabled |= joint;
ford_bp_canfd = (param & 2U) != 0U;
ford_bp_lka_counter_seen = false;
ford_bp_control_counter_seen = false;
@@ -65,16 +62,6 @@ static bool ford_bp_configure(uint16_t param) {
static bool ford_bp_health(uint32_t now);
static bool ford_bp_permission(void) {
bool permitted = false;
if (ford_aol_enabled) {
permitted = (ford_aol_permission_mask() & 1U) != 0U;
} else {
permitted = lateral_controls_allowed() && controls_allowed && !brake_pressed && !gas_pressed;
}
return permitted;
}
static void ford_bp_rx(const CANPacket_t *msg) {
if (ford_bp_enabled && (msg->bus == 0U) && (GET_LEN(msg) == 8U)) {
const unsigned int ids[7] = {0x415U, 0x202U, 0x91U, 0x165U, 0x204U, 0x213U, 0x83U};
@@ -90,7 +77,7 @@ static void ford_bp_rx(const CANPacket_t *msg) {
controls_allowed = false;
}
// Permission losses remove the old actuator history before any later arm.
if (!ford_bp_permission() || !ford_bp_health(microsecond_timer_get())) {
if (!controls_allowed || brake_pressed || gas_pressed) {
ford_bp_withdraw();
}
}
@@ -126,7 +113,7 @@ static bool ford_bp_lka(const CANPacket_t *msg) {
((msg->data[7] & FORD_BP_VERSION_MASK) == FORD_BP_VERSION) &&
((msg->data[7] >> FORD_BP_CHECKSUM_SHIFT) == checksum);
const int shadow = to_signed((msg->data[5] << 8) | msg->data[6], 16);
const bool resync = (shadow == 0) && !ford_bp_permission();
const bool resync = (shadow == 0) && !controls_allowed;
if (ford_bp_lka_counter_seen && !resync) {
valid &= counter == ((ford_bp_lka_counter + 1U) % FORD_BP_COUNTER_MODULUS);
valid &= safety_get_ts_elapsed(now, ford_bp_shadow_us) >= 20000U;
@@ -206,7 +193,7 @@ static bool ford_bp_lateral(const CANPacket_t *msg) {
const float physical_cap = SAFETY_MIN(FORD_BP_CURVATURE_MAX, FORD_BP_LATERAL_ACCEL / (SAFETY_MAX(speed_max, 1.0F) * SAFETY_MAX(speed_max, 1.0F)));
const float relation_low = SAFETY_ABS(shadow) * speed_min * FORD_BP_GAIN_MIN * 2000.0F;
const float relation_high = SAFETY_ABS(shadow) * speed_max * FORD_BP_GAIN_MAX * 2000.0F;
valid &= ford_bp_permission() && ford_bp_health(now) && ford_bp_shadow_seen &&
valid &= lateral_controls_allowed() && controls_allowed && ford_bp_health(now) && ford_bp_shadow_seen &&
(safety_get_ts_elapsed(now, ford_bp_shadow_us) <= FORD_BP_SHADOW_AGE_US) &&
(path >= -1000) && (path <= 1047) && (SAFETY_ABS(path - ford_bp_path_last) <= delta) &&
((path == 0) || ((ford_bp_shadow_raw != 0) && ((path > 0) == (ford_bp_shadow_raw > 0)))) &&
@@ -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",
@@ -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)
+5 -31
View File
@@ -10,9 +10,7 @@ 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, pedal_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.hyundai.discovery import normal_bus_topology
from opendbc.car.values import BRANDS
from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
@@ -99,10 +97,8 @@ def _gm_fw_uses_normal_bus0() -> bool:
# **** for use live only ****
def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback,
cached_params: CarParamsT | None, *, known_gm_bus0: bool = False, known_ford_bus0: bool = False
cached_params: CarParamsT | None, *, known_gm_bus0: bool = False
) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
if known_ford_bus0 and (known_gm_bus0 or not ford_normal_bus0_queries()):
raise ValueError('Ford discovery topology no longer uses normal bus 0')
if known_gm_bus0 and not _gm_fw_uses_normal_bus0():
raise ValueError('GM discovery topology no longer uses normal bus 0')
fixed_fingerprint = os.environ.get('FINGERPRINT', "")
@@ -132,30 +128,15 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0,))
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing, query_brand='gm')
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
elif known_ford_bus0:
# A registered CAN-FD selection identifies Ford's query family, not live
# ECU authority. Keep normal CAN2 wiring during discovery: the unrelated
# all-brand OBD mux scan can produce a CAN2 error/interrupt storm.
set_obd_multiplexing(False)
warmup_remaining = 0.1 - (time.monotonic() - start_time)
if warmup_remaining > 0:
time.sleep(warmup_remaining)
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0,))
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing,
query_brand='ford', normal_bus_only=True)
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
elif (normal_brand := normal_bus_query_brand(
can_recv, normal_bus_topology,
gm_probe=pedal_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
@@ -217,19 +198,12 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
cached_params = None
explicit_candidate = forced_candidate if forced_candidate is not None else os.environ.get('FINGERPRINT', '')
known_gm_bus0 = (explicit_candidate in interface_names['gm'] and _gm_fw_uses_normal_bus0())
known_ford_bus0 = ford_normal_bus0_selection(explicit_candidate)
if known_gm_bus0 and cached_params is not None and (
cached_params.brand != 'gm' or cached_params.carFingerprint != explicit_candidate):
cached_params = None
if known_ford_bus0 and cached_params is not None and (
cached_params.brand != 'ford' or cached_params.carFingerprint != explicit_candidate):
cached_params = None
if known_gm_bus0:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(
can_recv, can_send, set_obd_multiplexing, cached_params, known_gm_bus0=True)
elif known_ford_bus0:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(
can_recv, can_send, set_obd_multiplexing, cached_params, known_ford_bus0=True)
else:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, cached_params)
@@ -1,45 +0,0 @@
"""Exact stock-ACC Chrysler AOL admission. Dashcam variants remain excluded."""
from opendbc.car import structs
from opendbc.car.chrysler.values import CAR, ChryslerFlags
CHRYSLER_AOL_EXPERIENCE = 32
CHRYSLER_AOL_CLASSIC = frozenset((
CAR.CHRYSLER_PACIFICA_2018_HYBRID, CAR.CHRYSLER_PACIFICA_2019_HYBRID,
CAR.CHRYSLER_PACIFICA_2018, CAR.CHRYSLER_PACIFICA_2020, CAR.DODGE_DURANGO,
CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019,
))
CHRYSLER_AOL_CARS = CHRYSLER_AOL_CLASSIC | {CAR.RAM_1500_5TH_GEN}
def qualified(cp, *, marked_only=False):
if (cp is None or cp.brand != 'chrysler' or cp.carFingerprint not in CHRYSLER_AOL_CARS or
cp.notCar or cp.passive or cp.dashcamOnly or not cp.pcmCruise or
cp.openpilotLongitudinalControl or cp.alphaLongitudinalAvailable or
cp.steerControlType != structs.CarParams.SteerControlType.torque or
len(cp.safetyConfigs) != 1 or
cp.alternativeExperience not in ((CHRYSLER_AOL_EXPERIENCE,) if marked_only else (0, CHRYSLER_AOL_EXPERIENCE))):
return False
flags = ChryslerFlags.HIGHER_MIN_STEERING_SPEED | ChryslerFlags.HAS_BSM | ChryslerFlags.STEERING_SPEED_BYPASS
jeep = cp.carFingerprint in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019)
if jeep:
flags |= ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE
expected = (1 if cp.carFingerprint == CAR.RAM_1500_5TH_GEN else
8 if cp.carFingerprint == CAR.CHRYSLER_PACIFICA_2019_HYBRID and cp.alternativeExperience == 32 else 0)
return bool(not int(cp.flags) & ~int(flags) and
cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.chrysler and
int(cp.safetyConfigs[0].safetyParam) in ((0, 4) if jeep else (expected,)) and
cp.lateralTuning.which() in (('torque',) if cp.carFingerprint == CAR.RAM_1500_5TH_GEN else ('pid', 'torque')))
def temporary_restriction(cp, state):
gears = structs.CarState.GearShifter
return bool(qualified(cp, marked_only=True) and state is not None and
state.canValid and not state.canTimeout and not state.steerFaultPermanent and
(state.gearShifter not in (gears.drive, gears.low) or state.steerFaultTemporary or
state.standstill or state.vEgo < cp.minSteerSpeed))
def lkas_button_supported(cp):
# Dom's Chrysler whitelist did not enable LKAS AOL. Preserve that feature
# disposition until an intentional end-to-end activation requirement exists.
return False
@@ -1,25 +0,0 @@
"""Optional stock-ACC stop retention for exactly the two Grand Cherokee ports."""
def brake_hold_supported(cp):
from opendbc.car import structs
from opendbc.car.chrysler.values import CAR, ChryslerFlags
return bool(cp is not None and cp.brand == "chrysler" and
cp.carFingerprint in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019) and
cp.flags & ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE and cp.pcmCruise and
not cp.openpilotLongitudinalControl and not cp.passive and not cp.dashcamOnly and not cp.notCar and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.chrysler and
cp.safetyConfigs[0].safetyParam in (0, 4))
def apply_brake_hold(cp, requested):
if brake_hold_supported(cp):
cp.safetyConfigs[0].safetyParam = 4 if requested else 0
def create_das_3_command(packer, counter_offset, brake_decel, das_3):
values = das_3.copy()
values.update(ACC_AVAILABLE=1, ACC_ACTIVE=1, ACC_GO=0, ACC_STANDSTILL=0, ACC_DECEL_REQ=1,
ACC_DECEL=brake_decel, ACC_BRK_PREP=0, ENGINE_TORQUE_REQUEST_MAX=0, GR_MAX_REQ=2,
COUNTER=(int(das_3["COUNTER"]) + counter_offset) % 16)
return packer.make_can_msg("DAS_3", 0, values)
@@ -1,12 +1,7 @@
import os
import time
from opendbc.can import CANPacker
from opendbc.car import Bus, DT_CTRL
from opendbc.car.lateral import apply_meas_steer_torque_limits
from opendbc.car.chrysler import chryslercan
from opendbc.car.chrysler.aol_contract import qualified as aol_qualified
from opendbc.car.chrysler.brake_hold import brake_hold_supported, create_das_3_command
from opendbc.car.chrysler.values import CUSW_CARS, RAM_CARS, RAM_DT, CarControllerParams, ChryslerFlags
from opendbc.car.structs import CarState
from opendbc.car.interfaces import CarControllerBase
@@ -21,11 +16,6 @@ class CarController(CarControllerBase):
self.last_lkas_falling_edge = 0
self.lkas_control_bit_prev = False
self.last_button_frame = 0
self.jeep_hold_replay = "REPLAY" in os.environ
self.jeep_decel = -2.0
self.last_das_3_counter = -1
self.jeep_hold_cancelled = False
self.jeep_cruise_was_active = False
self.packer = CANPacker(dbc_names[Bus.pt])
self.params = CarControllerParams(CP)
@@ -43,17 +33,12 @@ class CarController(CarControllerBase):
das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0
# ACC cancellation
pause_stock_acc = bool(aol_qualified(self.CP, marked_only=True) and
CS.aol_pause_longitudinal and 0 < CS.aol_pause_longitudinal_stamp_ns <= now_nanos and
now_nanos - CS.aol_pause_longitudinal_stamp_ns <= 30_000_000 and
CS.out.canValid and not CS.out.canTimeout)
if CC.cruiseControl.cancel or (pause_stock_acc and CS.out.cruiseState.enabled):
if CC.cruiseControl.cancel:
self.last_button_frame = self.frame
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True))
# ACC resume from standstill
elif (CC.cruiseControl.resume and not pause_stock_acc and
(not aol_qualified(self.CP, marked_only=True) or CS.out.cruiseState.enabled)):
elif CC.cruiseControl.resume:
self.last_button_frame = self.frame
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True))
@@ -109,14 +94,6 @@ class CarController(CarControllerBase):
if _projection is not None:
_projection.emit('chrysler.LKAS_COMMAND', apply_torque, lkas_control_bit, can_sends[-1])
if brake_hold_supported(self.CP) and self.CP.safetyConfigs[0].safetyParam == 4:
withdraw = bool(aol_qualified(self.CP, marked_only=True) and CS.aol_pause_longitudinal and
0 < CS.aol_pause_longitudinal_stamp_ns <= now_nanos and
now_nanos - CS.aol_pause_longitudinal_stamp_ns <= 30_000_000)
self.update_jeep_brake_hold(CC, CS, now_nanos, can_sends, withdraw=withdraw)
else:
CS.brake_hold = False
self.frame += 1
new_actuators = CC.actuators.as_builder()
@@ -126,43 +103,3 @@ class CarController(CarControllerBase):
if _projection is not None:
_projection.finish(new_actuators.torque)
return new_actuators, can_sends
def update_jeep_brake_hold(self, CC, CS, now_nanos, can_sends, *, withdraw=False):
# CAN Event/parser timestamps are BOOTTIME. Card passes MONOTONIC for
# ordinary live Chrysler controllers, and original CAN time during replay.
# Select the established replay contract explicitly; never infer an offset
# or renew the parser's original source timestamp at command time.
now_boot_ns = now_nanos if self.jeep_hold_replay else time.clock_gettime_ns(time.CLOCK_BOOTTIME)
if (not CS.das_3 or not CS.out.canValid or CS.out.canTimeout or
not 0 <= now_boot_ns - CS.das_3_timestamp <= 100_000_000):
CS.brake_hold = False
return
counter = int(CS.das_3["COUNTER"])
changed = counter != self.last_das_3_counter
self.last_das_3_counter = counter
active = CS.out.cruiseState.enabled
if active and not self.jeep_cruise_was_active:
self.jeep_hold_cancelled = False
self.jeep_cruise_was_active = active
if CC.cruiseControl.cancel or CS.jeep_driver_cancel or withdraw:
self.jeep_hold_cancelled = True
if self.jeep_hold_cancelled:
CS.brake_hold = False
return
decel = float(CS.das_3["ACC_DECEL"])
if not CS.brake_hold and CS.out.cruiseState.enabled and decel < -0.5 and CS.out.standstill:
CS.brake_hold = True
self.jeep_decel = -2.0
if (CC.cruiseControl.cancel or CS.jeep_driver_cancel or withdraw or CS.out.gasPressed or CS.out.brakePressed or
CS.out.gearShifter != CarState.GearShifter.drive or not CS.out.standstill or
not CS.out.cruiseState.available or CS.out.accFaulted):
CS.brake_hold = False
return
if not CS.brake_hold:
return
if CS.out.cruiseState.enabled:
self.jeep_decel = max(-3.0, min(-0.5, min(self.jeep_decel, decel)))
return
can_sends.append(create_das_3_command(self.packer, 2 if changed else 3, self.jeep_decel, CS.das_3))
if self.frame % 10 == 0:
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, 0, resume=True))
+1 -22
View File
@@ -1,6 +1,5 @@
from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, create_button_events, structs
from opendbc.car.chrysler.aol_contract import qualified as aol_qualified
from opendbc.car.chrysler.values import CUSW_CARS, DBC, STEER_THRESHOLD, RAM_CARS, ChryslerFlags
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
@@ -24,13 +23,6 @@ class CarState(CarStateBase):
self.shifter_values = can_define.dv["GEAR"]["PRNDL"]
self.distance_button = 0
self.aol_pause_longitudinal = False
self.aol_pause_longitudinal_stamp_ns = 0
self.aol_buttons = {ButtonType.mainCruise: 0, ButtonType.cancel: 0}
self.brake_hold = False
self.das_3 = {}
self.das_3_timestamp = 0
self.jeep_driver_cancel = False
def update(self, can_parsers) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -88,11 +80,6 @@ class CarState(CarStateBase):
ret.cruiseState.nonAdaptive = cp_cruise.vl["DAS_4"]["ACC_STATE"] in (1, 2) # 1 NormalCCOn and 2 NormalCCSet
ret.cruiseState.standstill = cp_cruise.vl["DAS_3"]["ACC_STANDSTILL"] == 1
ret.accFaulted = cp_cruise.vl["DAS_3"]["ACC_FAULTED"] != 0
if self.CP.flags & ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE:
self.das_3 = dict(cp_cruise.vl["DAS_3"])
self.jeep_driver_cancel = bool(cp.vl["CRUISE_BUTTONS"]["ACC_Cancel"])
self.das_3_timestamp = cp_cruise.ts_nanos["DAS_3"]["COUNTER"]
ret.brakeHoldActive = self.brake_hold
if self.CP.carFingerprint in RAM_CARS:
# Auto High Beam isn't Located in this message on chrysler or jeep currently located in 729 message
@@ -110,15 +97,7 @@ class CarState(CarStateBase):
self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
button_events = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
if aol_qualified(self.CP, marked_only=True):
buttons = cp.vl["CRUISE_BUTTONS"]
values = {ButtonType.mainCruise: bool(buttons["Cruise_OnOff"] or buttons["ACC_OnOff"]),
ButtonType.cancel: bool(buttons["ACC_Cancel"])}
for button, value in values.items():
button_events += create_button_events(value, self.aol_buttons[button], {1: button})
self.aol_buttons[button] = value
ret.buttonEvents = button_events
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
return ret
@@ -57,7 +57,6 @@ class CarInterface(CarInterfaceBase):
elif candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
ret.steerActuatorDelay = 0.2
ret.flags |= ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE.value
ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
@@ -1,235 +0,0 @@
"""Real Jeep parser/controller stock-stop retention; no system speed authority."""
import os
import unittest
from unittest.mock import patch
from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, gen_empty_fingerprint, structs
from opendbc.car.chrysler.brake_hold import apply_brake_hold, brake_hold_supported
from opendbc.car.chrysler.interface import CarInterface
from opendbc.car.chrysler.values import CAR, DBC
def params(candidate=CAR.JEEP_GRAND_CHEROKEE, *, enabled=True):
cp = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False)
apply_brake_hold(cp, enabled)
return cp
class PhysicalCan:
def __init__(self, ci):
ci.update([])
self.ci = ci
self.packers = {bus: CANPacker(parser.dbc.name) for bus, parser in ci.can_parsers.items()}
def frames(self, tick, *, active=True, decel=-2.0, main=True, gas=0.0, brake=0, speed=0.0, gear=4, fault=0, omit=()):
values = {
'DAS_3': {
'ACC_ACTIVE': int(active),
'ACC_AVAILABLE': int(main),
'ACC_DECEL': decel,
'ACC_STANDSTILL': 1,
'ACC_FAULTED': fault,
'ENGINE_TORQUE_REQUEST': 13.25,
'DISABLE_FUEL_SHUTOFF': 1,
'COLLISION_BRK_PREP': 1,
'COUNTER': (14 + tick // 2) % 16,
},
'DAS_4': {'ACC_SET_SPEED_KPH': 100, 'ACC_STATE': 3},
'GEAR': {'PRNDL': gear},
'ECM_5': {'Accelerator_Position': gas},
'ESP_1': {'Brake_Pedal_State': brake},
'SPEED_1': {'SPEED_LEFT': speed, 'SPEED_RIGHT': speed},
}
return [
self.packers[bus].make_can_msg(parser.dbc.addr_to_msg[address].name, parser.bus, values.get(parser.dbc.addr_to_msg[address].name, {}))
for bus, parser in self.ci.can_parsers.items()
for address in sorted(parser.addresses)
if parser.dbc.addr_to_msg[address].name not in omit
]
def apply_at_boot(ci, command, now):
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=now):
return ci.apply(command.as_reader(), now)
class TestJeepBrakeHold(unittest.TestCase):
def setUp(self):
prior = os.environ.pop('REPLAY', None)
def restore():
if prior is None:
os.environ.pop('REPLAY', None)
else:
os.environ['REPLAY'] = prior
self.addCleanup(restore)
def ready(self, candidate=CAR.JEEP_GRAND_CHEROKEE):
ci = CarInterface(params(candidate))
stream = PhysicalCan(ci)
cc = structs.CarControl(enabled=True)
for tick in range(20):
ci.update([(100_000_000_000 + tick * 10_000_000, stream.frames(tick))])
apply_at_boot(ci, cc, 100_000_000_000 + tick * 10_000_000)
self.assertTrue(ci.CS.out.canValid and ci.CS.brake_hold)
return ci, stream, cc
def test_two_exact_platforms_stock_ownership_default_off_and_siblings(self):
for candidate in CAR:
for release in (False, True):
cp = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, release, False)
expected = candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019)
self.assertEqual(brake_hold_supported(cp), expected)
before = cp.to_dict()
apply_brake_hold(cp, True)
self.assertEqual(cp.safetyConfigs[0].safetyParam, 4 if expected else before['safetyConfigs'][0]['safetyParam'])
self.assertFalse(cp.openpilotLongitudinalControl)
self.assertTrue(cp.pcmCruise)
apply_brake_hold(cp, False)
self.assertEqual(cp.to_dict(), before)
def test_real_parser_copied_fields_decel_counter_wrap_and_repeat(self):
for candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
ci, stream, cc = self.ready(candidate)
decoder = CANParser(DBC[candidate][Bus.pt], [('DAS_3', 0)], 0)
for tick in range(20, 55):
now = 100_000_000_000 + tick * 10_000_000
previous = ci.CC.last_das_3_counter
ci.update([(now, stream.frames(tick, active=False))])
_, frames = apply_at_boot(ci, cc, now)
hold = [frame for frame in frames if frame[0] == 0x1F4]
self.assertEqual(len(hold), 1)
decoder.update([(now, hold)])
parsed = decoder.vl['DAS_3']
stock = ci.CS.das_3
self.assertEqual(parsed['COUNTER'], (int(stock['COUNTER']) + (2 if int(stock['COUNTER']) != previous else 3)) % 16)
for name in ('ENGINE_TORQUE_REQUEST', 'DISABLE_FUEL_SHUTOFF', 'COLLISION_BRK_PREP', 'ACC_FAULTED'):
self.assertEqual(parsed[name], stock[name])
for name, expected in {
'ACC_ACTIVE': 1,
'ACC_AVAILABLE': 1,
'ACC_GO': 0,
'ACC_STANDSTILL': 0,
'ACC_DECEL_REQ': 1,
'GR_MAX_REQ': 2,
'ACC_BRK_PREP': 0,
'ENGINE_TORQUE_REQUEST_MAX': 0,
}.items():
self.assertEqual(parsed[name], expected)
self.assertAlmostEqual(parsed['ACC_DECEL'], -2.0, delta=0.004885)
resumes = [frame for frame in frames if frame[0] == 0x23B]
self.assertEqual(bool(resumes), tick % 10 == 0)
def test_driver_main_fault_and_source_withdrawal(self):
for stimulus in ('gas', 'brake', 'speed', 'gear', 'main', 'fault', 'cancel', 'stale'):
ci, stream, cc = self.ready()
args = (
{stimulus: {'gas': 1.0, 'brake': 1, 'speed': 1.0, 'gear': 1, 'main': False, 'fault': 1}.get(stimulus)} if stimulus not in ('cancel', 'stale') else {}
)
now = 100_200_000_000
if stimulus == 'cancel':
cc.cruiseControl.cancel = True
ci.update([(now, stream.frames(20, active=False, **args))])
if stimulus == 'stale':
now += 100_000_001
_, frames = apply_at_boot(ci, cc, now)
self.assertFalse(ci.CS.brake_hold, stimulus)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames), stimulus)
def test_stock_braking_strength_clamped_to_original_envelope(self):
ci, stream, cc = self.ready()
for tick in range(20, 30):
now = 100_000_000_000 + tick * 10_000_000
ci.update([(now, stream.frames(tick, decel=-4.0))])
apply_at_boot(ci, cc, now)
self.assertEqual(ci.CC.jeep_decel, -3.0)
def test_real_parser_crc_fault_withdraws_hold(self):
ci, stream, cc = self.ready()
for tick in range(20, 32):
now = 100_000_000_000 + tick * 10_000_000
frames = stream.frames(tick, active=False)
for index, (address, data, bus) in enumerate(frames):
if address == 0x1F4:
invalid = bytearray(data)
invalid[-1] ^= 1
frames[index] = (address, bytes(invalid), bus)
ci.update([(now, frames)])
_, emitted = apply_at_boot(ci, cc, now)
self.assertGreater(now - ci.CS.das_3_timestamp, 100_000_000)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in emitted))
def test_commanded_cancel_requires_fresh_stock_acc_engagement(self):
ci, stream, cc = self.ready()
cc.cruiseControl.cancel = True
ci.update([(100_200_000_000, stream.frames(20))])
apply_at_boot(ci, cc, 100_200_000_000)
cc.cruiseControl.cancel = False
for tick in range(21, 25):
now = 100_000_000_000 + tick * 10_000_000
ci.update([(now, stream.frames(tick))])
apply_at_boot(ci, cc, now)
self.assertFalse(ci.CS.brake_hold)
ci.update([(100_250_000_000, stream.frames(25, active=False))])
apply_at_boot(ci, cc, 100_250_000_000)
ci.update([(100_260_000_000, stream.frames(26))])
apply_at_boot(ci, cc, 100_260_000_000)
self.assertTrue(ci.CS.brake_hold)
def test_actual_factory_default_off_and_siblings_emit_no_hold(self):
for candidate in CAR:
for requested in (False, True):
if requested and candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
continue
ci = CarInterface(params(candidate, enabled=requested))
stream = PhysicalCan(ci)
cc = structs.CarControl()
for tick in range(20):
now = 100_000_000_000 + tick * 10_000_000
ci.update([(now, stream.frames(tick, active=False))])
_, frames = apply_at_boot(ci, cc, now)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
def test_actual_parser_reverse_then_drive_does_not_restore_disabled_stock_long(self):
ci, stream, cc = self.ready()
ci.update([(100_200_000_000, stream.frames(20, active=False, gear=2))])
_, frames = apply_at_boot(ci, cc, 100_200_000_000)
self.assertEqual(ci.CS.out.gearShifter, structs.CarState.GearShifter.reverse)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
ci.update([(100_210_000_000, stream.frames(21, active=False, gear=4))])
_, frames = apply_at_boot(ci, cc, 100_210_000_000)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
def test_original_can_boot_age_is_independent_of_controller_monotonic_time(self):
ci, stream, cc = self.ready()
boot_ns = 100_200_000_000
ci.update([(boot_ns, stream.frames(20, active=False))])
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=boot_ns):
_, frames = ci.apply(cc.as_reader(), boot_ns - 9_000_000_000)
self.assertTrue(ci.CS.brake_hold)
self.assertTrue(any(frame[0] == 0x1F4 for frame in frames))
def test_original_boot_source_future_and_stale_are_not_restamped(self):
for offset in (-1, 100_000_001):
ci, stream, cc = self.ready()
source = 100_200_000_000
ci.update([(source, stream.frames(20, active=False))])
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=source + offset):
_, frames = ci.apply(cc.as_reader(), source)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
def test_replay_uses_original_can_time_in_the_explicit_replay_domain(self):
with patch.dict(os.environ, {'REPLAY': '1'}):
ci, stream, cc = self.ready()
source = 100_200_000_000
ci.update([(source, stream.frames(20, active=False))])
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', side_effect=AssertionError('no host clock in original-time replay')):
_, frames = ci.apply(cc.as_reader(), source)
self.assertTrue(any(frame[0] == 0x1F4 for frame in frames))
@@ -12,8 +12,6 @@ Ecu = CarParams.Ecu
class ChryslerSafetyFlags(IntFlag):
RAM_DT = 1
RAM_HD = 2
AOL_PACIFICA_HYBRID_SET_GUARD = 8
JEEP_BRAKE_HOLD = 4
class ChryslerFlags(IntFlag):
@@ -21,7 +19,6 @@ class ChryslerFlags(IntFlag):
HIGHER_MIN_STEERING_SPEED = 1
HAS_BSM = 2 # blind spot monitoring
STEERING_SPEED_BYPASS = 4
JEEP_BRAKE_HOLD_AVAILABLE = 8
@dataclass
+7 -44
View File
@@ -1,56 +1,18 @@
from opendbc.car import structs
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.ford.values import CAR, FordFlags
AOL_EXPERIENCE = 32
CLASSIC = frozenset((CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_FOCUS_MK4,
CAR.FORD_MAVERICK_MK1, CAR.FORD_EXPLORER_MK6))
GENERIC_CANFD = frozenset((CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14,
CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2))
MANUAL = int(FordSafetyFlags.MANUAL)
BASE_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
ANGLE = 128
ANGLE_BASE_WORDS = frozenset((18, 19, 32, 33, 66, 67))
AOL_WORDS = (BASE_WORDS | frozenset(word | ANGLE for word in ANGLE_BASE_WORDS))
AOL_WORDS |= frozenset(word | MANUAL for word in AOL_WORDS)
def aol_safety_config(cp):
if len(cp.safetyConfigs) not in (1, 2):
return None
if len(cp.safetyConfigs) == 2:
passive = cp.safetyConfigs[0]
if passive.safetyModel != structs.CarParams.SafetyModel.noOutput or passive.safetyParam != 0:
return None
owner = cp.safetyConfigs[-1]
return owner if owner.safetyModel == structs.CarParams.SafetyModel.ford else None
def base_safety_param(cp):
owner = aol_safety_config(cp)
if owner is None:
return -1
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
return -1
word = int(owner.safetyParam)
if (word & MANUAL and not manual) or (manual and cp.alternativeExperience == AOL_EXPERIENCE and not word & MANUAL):
return -1
if word & ANGLE:
if cp.alternativeExperience == 0:
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
if not angle_qualified(cp):
return -1
base = 18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else (66 if cp.carFingerprint in GENERIC_CANFD else 32)
return base | int(cp.openpilotLongitudinalControl)
if word not in AOL_WORDS:
return -1
word &= ~ANGLE
return word & ~MANUAL
AOL_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
def qualified(cp, *, marked_only=False):
if (cp.brand != 'ford' or cp.carFingerprint not in tuple(CAR) or cp.passive or cp.notCar or cp.dashcamOnly or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
cp.transmissionType != structs.CarParams.TransmissionType.automatic or len(cp.safetyConfigs) != 1 or
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
return False
flags = int(cp.flags) & ~int(FordFlags.HAS_BSM)
@@ -68,9 +30,10 @@ def qualified(cp, *, marked_only=False):
base = 12
else:
return False
safety = aol_safety_config(cp)
return bool(cp.pcmCruise and safety is not None and
base_safety_param(cp) == (base | int(cp.openpilotLongitudinalControl)))
safety = cp.safetyConfigs[0]
return (cp.pcmCruise and
safety.safetyModel == structs.CarParams.SafetyModel.ford and
safety.safetyParam == (base | int(cp.openpilotLongitudinalControl)))
def temporary_restriction(cp, state):
+20 -54
View File
@@ -75,9 +75,6 @@ class CarController(CarControllerBase):
self.manual_turn_inputs = None
self.mache_lateral = MachELateralController(CP) if mache_qualified(CP) else None
self.mache_extended_announced = False
self.mache_ui_owned = False
self.hands_free_cluster_input = None
self.cluster_extended_announced = False
self.generic_canfd = generic_canfd_qualified(CP)
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
self.new_port_curvature = create_new_port_curvature(CP)
@@ -101,10 +98,6 @@ class CarController(CarControllerBase):
def update(self, CC, CS, now_nanos):
can_sends = []
if self.hands_free_cluster_input is not None:
self.hands_free_cluster_input.update()
if not CC.latActive:
self.mache_ui_owned = False
if (self.mache_lateral is not None or self.classic_lateral is not None or self.new_port_curvature is not None) and self.manual_turn_inputs is not None:
self.manual_turn_inputs.update()
self.manual_turn_inputs.apply_blend_settings(self)
@@ -142,11 +135,9 @@ class CarController(CarControllerBase):
### lateral control ###
# send steer msg at 20Hz
if self.bp_lat is not None:
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)
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,7 +156,6 @@ class CarController(CarControllerBase):
demanded = self.mache_lateral.update(CC, CS, actuators) if self.mache_extended_announced else FordLateralResult()
lateral = bounded_command(self.mache_lateral, demanded, previous, CS.out.vEgoRaw, previous_path_angle)
self.mache_lateral_demand = demanded
self.mache_ui_owned |= bool(lateral.active)
self.apply_curvature_last = lateral.curvature
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(mache_can.create_lat_ctl2_msg(
@@ -178,20 +168,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:
# 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,
@@ -262,8 +245,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:
@@ -294,11 +276,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))
@@ -343,38 +322,25 @@ class CarController(CarControllerBase):
self.accel = accel
self.gas = gas
# Track the actual outgoing LKA announcement for every CAN-FD display owner.
if any(address == 0x3CA for address, _, _ in can_sends):
self.cluster_extended_announced = True
### ui ###
# 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:
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != ui_enabled) or (self.steer_alert_last != steer_alert)
# send lkas ui msg at 1Hz or if ui state changes
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, ui_enabled, steer_alert, hud_control, CS.lkas_status_stock_values))
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert)
# send lkas ui msg at 1Hz or if ui state changes
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
# send acc ui msg at 5Hz or if ui state changes
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
send_ui = True
self.distance_bar_frame = self.frame
# send acc ui msg at 5Hz or if ui state changes
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
send_ui = True
self.distance_bar_frame = self.frame
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
show_distance_bars = self.frame - self.distance_bar_frame < 400
hands_free_cluster = bool(self.cluster_extended_announced and
self.hands_free_cluster_input is not None and self.hands_free_cluster_input.enabled)
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, ui_enabled,
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
hud_control, CS.acc_tja_status_stock_values, hands_free_cluster))
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
show_distance_bars = self.frame - self.distance_bar_frame < 400
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
hud_control, CS.acc_tja_status_stock_values))
self.main_on_last = main_on
self.lkas_enabled_last = ui_enabled
self.lkas_enabled_last = CC.latActive
self.steer_alert_last = steer_alert
self.lead_distance_bars_last = hud_control.leadDistanceBars
-20
View File
@@ -35,13 +35,10 @@ class CarState(CarStateBase):
self.lc_button = 0
self.cancel_button = False
self.cancel_resume_button = FordStockCruiseButton()
self.cruise_buttons = {ButtonType.accelCruise: False, ButtonType.decelCruise: False}
self.steering_angle_offset_deg = 0.0
self.lkas_available = False
self.lkas_available_ts_nanos = 0
self.lateral_motion_control = None
self.bluepilot_speed_samples = [0] * 6 if CP.safetyConfigs and CP.safetyConfigs[-1].safetyParam & 128 else None
self.bluepilot_speed_window = None
def get_car_gps(self):
return self.car_gps_tracker.get()
@@ -65,14 +62,6 @@ class CarState(CarStateBase):
# car speed
ret.vEgoRaw = cp.vl["BrakeSysFeatures"]["Veh_V_ActlBrk"] * CV.KPH_TO_MS
if self.bluepilot_speed_samples is not None:
# Match Panda's six raw ABS speed samples, including its millimetre/second rounding.
for value in cp.vl_all["BrakeSysFeatures"]["Veh_V_ActlBrk"]:
self.bluepilot_speed_samples.append(math.floor(value * CV.KPH_TO_MS * 1000 + .5))
self.bluepilot_speed_samples = self.bluepilot_speed_samples[-6:]
samples = self.bluepilot_speed_samples
step = max(abs(b - a) for a, b in zip(samples, samples[1:], strict=False)) / 1000
self.bluepilot_speed_window = (min(samples) / 1000, max(samples) / 1000, step)
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
ret.yawRate = cp.vl["Yaw_Data_FD1"]["VehYaw_W_Actl"]
ret.standstill = cp.vl["DesiredTorqBrk"]["VehStop_D_Stat"] == 1
@@ -180,13 +169,6 @@ class CarState(CarStateBase):
if self.CP.openpilotLongitudinalControl:
button_events += create_button_events(self.cancel_button, prev_cancel_button, {1: ButtonType.cancel})
for button, signals in (
(ButtonType.accelCruise, ('CcAslButtnSetIncPress', 'CcAslButtnResIncPress', 'CcAslButtnIndxIncPress')),
(ButtonType.decelCruise, ('CcAslButtnSetDecPress', 'CcAslButtnResDecPress', 'CcAslButtnIndxDecPress')),
):
pressed = any(self.buttons_stock_values[signal] for signal in signals)
button_events += create_button_events(pressed, self.cruise_buttons[button], {1: button})
self.cruise_buttons[button] = pressed
ret.buttonEvents = button_events
@@ -225,8 +207,6 @@ class CarState(CarStateBase):
gear = ("Gear_Shift_by_Wire_FD1" if CP.flags & FordFlags.CANFD else
"TransGearData" if CP.flags & FordFlags.ALT_STEER_ANGLE else "PowertrainData_10")
pt_messages.append((gear, 10))
if CP.transmissionType == TransmissionType.manual:
pt_messages.append(("BCM_Lamp_Stat_FD1", 10))
if CP.flags & FordFlags.LKA_STEERING:
pt_messages.append(("Lane_Assist_Data3_FD1", 30))
cam_messages.append(("LateralMotionControl", 20))
@@ -6,7 +6,6 @@ CREDITS.md and THIRD_PARTY_NOTICES.md; this module only selects admitted callers
"""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import ExplorerLateralController, bounded_command as bounded_command, qualified as explorer_qualified
from opendbc.car.ford.lateral_strategy import FordLateralController
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
@@ -17,6 +16,8 @@ CLASSIC_EXTENDED_CARS = GENERIC_CLASSIC_CARS | {CAR.FORD_EXPLORER_MK6}
def qualified(cp):
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
return False
if cp.carFingerprint == CAR.FORD_EXPLORER_MK6:
return explorer_qualified(cp)
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CLASSIC_CARS or
@@ -28,7 +29,7 @@ def qualified(cp):
return False
safety = cp.safetyConfigs[-1]
expected = int(FordSafetyFlags.CLASSIC_EXTENDED | (FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
class GenericClassicLateralController(FordLateralController):
@@ -1,86 +0,0 @@
"""Normal-bus discovery for explicitly selected Ford CAN-FD vehicles."""
import time
from opendbc.car.ford.values import CAR, FordFlags
def normal_bus0_queries():
# Verify both configured and effective requests before omitting the OBD sweep.
# Essential queries use normal bus0 and its exact auxiliary Panda bus4 copy.
# As-built remains bus0; bus1 logging cannot provide unique evidence.
from opendbc.car import fw_versions
configured = fw_versions.FW_QUERY_CONFIGS['ford'].requests
effective = [request for brand, _, request in fw_versions.REQUESTS if brand == 'ford']
if not configured or not effective:
return False
def signature(request):
return (tuple(request.request), tuple(request.response), request.rx_offset, frozenset(request.whitelist_ecus))
for requests in (configured, effective):
normal = {signature(request) for request in requests if request.bus == 0}
essential = {signature(request) for request in requests if request.bus == 0 and not request.logging}
auxiliary = {signature(request) for request in requests if request.bus == 4 and not request.logging}
if not normal or not essential or auxiliary != essential:
return False
for request in requests:
if request.bus == 0:
continue
if request.bus == 4 and not request.logging and signature(request) in essential:
continue
if request.bus == 1 and request.logging and signature(request) in normal:
continue
return False
return True
def normal_bus0_selection(candidate):
return bool(candidate in CAR and CAR(candidate).config.flags & FordFlags.CANFD and
not CAR(candidate).config.flags & FordFlags.LKA_STEERING and normal_bus0_queries())
# 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'
# A Ford CAN1 frame can invalidate an HKG-only layout on the first read.
# Complete Ford evidence within the original time/read budget, not a new probe.
while not malformed and received != NORMAL_BUS0_LAYOUT and reads < 24 and clock() < deadline:
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
@@ -11,7 +11,6 @@ from typing import Protocol
import numpy as np
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult, FORD_CURVATURE_LIMITS, STEER_DT
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags, FordSafetyFlags
@@ -35,6 +34,8 @@ class ExplorerLateralController(FordLateralController):
def qualified(CP) -> bool:
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_EXPLORER_MK6 or
CP.flags & ~int(FordFlags.HAS_BSM) or CP.passive or CP.dashcamOnly or CP.notCar or
CP.alternativeExperience not in (0, 32) or len(CP.safetyConfigs) not in (1, 2)):
@@ -44,7 +45,7 @@ def qualified(CP) -> bool:
return False
safety = CP.safetyConfigs[-1]
expected = int(FordSafetyFlags.EXPLORER_EXTENDED | (FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0))
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == expected
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
class CurvatureOwner(Protocol):
+2 -5
View File
@@ -32,11 +32,10 @@ def create_lka_msg(packer, CAN: CanBus, active=False, apply_angle=0., direction=
if transit:
values = {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0., "LdwActvIntns_D_Req": 3}
if active:
# 5.8 degrees rounds outside native raw 24..4072 at 0.05 mrad/count.
values = {
"LkaDrvOvrrd_D_Rq": 0,
"LkaActvStats_D2_Req": direction,
"LaRefAng_No_Req": max(-101.2, min(101.2, math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.)),
"LaRefAng_No_Req": math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.,
"LaRampType_B_Req": int(abs(apply_angle) >= 5.),
"LaCurvature_No_Calc": max(-0.01023, min(0.01023, curvature)),
"LdwActvStats_D_Req": 0,
@@ -146,7 +145,7 @@ def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: fl
def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool, standstill: bool,
show_distance_bars: bool, hud_control, stock_values: dict, hands_free_cluster: bool = False):
show_distance_bars: bool, hud_control, stock_values: dict):
"""
Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam
assist status.
@@ -162,8 +161,6 @@ def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw
status = 3 # ActiveInterventionLeft
elif hud_control.rightLaneDepart:
status = 4 # ActiveInterventionRight
elif hands_free_cluster:
status = 7 # Hands-free assistance display
else:
status = 2 # Active
elif main_on:
@@ -4,7 +4,6 @@ The reused BluePilot-derived strategy retains its original attribution in
lateral_strategy.py, CREDITS.md and THIRD_PARTY_NOTICES.md.
"""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
from opendbc.car.ford.lateral_strategy import FordLateralController
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
@@ -15,6 +14,8 @@ SOURCE_ACCEL_CAP = 3.0 - 9.81 * .06
def qualified(cp):
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
return False
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CANFD_CARS or
not cp.flags & FordFlags.CANFD or cp.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
@@ -26,7 +27,7 @@ def qualified(cp):
s = cp.safetyConfigs[-1]
expected = int(FordSafetyFlags.GENERIC_CANFD_EXTENDED | FordSafetyFlags.CANFD |
(FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
return s.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
return s.safetyModel == structs.CarParams.SafetyModel.ford and s.safetyParam == expected
class GenericCanfdLateralController(FordLateralController):
+10 -10
View File
@@ -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,
@@ -60,12 +55,12 @@ class CarInterface(CarInterfaceBase):
new_port = bool(ret.flags & FordFlags.NEW_PORT)
if candidate in GENERIC_CANFD_CARS:
ret.alphaLongitudinalAvailable = True
ret.alphaLongitudinalAvailable = not is_release
ret.steerActuatorDelay = 0.22
longitudinal_requested = bool(alpha_long)
longitudinal_requested = bool(alpha_long and not is_release)
elif candidate == CAR.FORD_MUSTANG_MACH_E_MK1:
ret.alphaLongitudinalAvailable = True
longitudinal_requested = bool(alpha_long)
ret.alphaLongitudinalAvailable = not is_release
longitudinal_requested = bool(alpha_long and not is_release)
elif candidate in CLASSIC_EXTENDED_CARS:
# Original shared classic ownership is explicit stock/OP-long selection.
# Actuator delay is live; modern LongControl already supplies the original P=0.
@@ -73,7 +68,7 @@ class CarInterface(CarInterfaceBase):
ret.steerActuatorDelay = 0.22
longitudinal_requested = bool(alpha_long)
else:
ret.alphaLongitudinalAvailable = True if new_port else ret.radarUnavailable
ret.alphaLongitudinalAvailable = (not is_release if ret.flags & FordFlags.CANFD else True) if new_port else ret.radarUnavailable
longitudinal_requested = bool(alpha_long and ret.alphaLongitudinalAvailable) if new_port else alpha_long or not ret.radarUnavailable
if longitudinal_requested:
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.LONG_CONTROL.value
@@ -144,6 +139,11 @@ class CarInterface(CarInterfaceBase):
and ret.safetyConfigs[-1].safetyParam in (2, 3)
)
if mach_e_extended:
if is_release:
# The native CANFD LONG profile is DEBUG-only; RELEASE stays stock.
ret.alphaLongitudinalAvailable = False
ret.openpilotLongitudinalControl = False
ret.safetyConfigs[-1].safetyParam = FordSafetyFlags.CANFD.value
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.MACH_E_EXTENDED.value
if (candidate in GENERIC_CANFD_CARS and ret.flags & FordFlags.CANFD and
not ret.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) and
@@ -17,7 +17,6 @@ from dataclasses import replace
import numpy as np
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.lateral import MAX_LATERAL_JERK
from opendbc.car.ford.lateral_strategy import (
@@ -100,6 +99,8 @@ class MachELateralController(FordLateralController):
def qualified(CP) -> bool:
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or
not CP.flags & FordFlags.CANFD or CP.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
CP.passive or CP.dashcamOnly or CP.notCar or CP.alternativeExperience not in (0, 32) or
@@ -110,7 +111,7 @@ def qualified(CP) -> bool:
return False
safety = CP.safetyConfigs[-1]
return (safety.safetyModel == structs.CarParams.SafetyModel.ford and
base_safety_param(CP) == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
safety.safetyParam == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
(FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0)))
@@ -7,7 +7,6 @@ from dataclasses import replace
import math
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult
from opendbc.car.ford.values import CAR, FordFlags
@@ -23,7 +22,7 @@ def qualified(cp):
if (profile is None or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or
cp.alternativeExperience not in (0, 32) or not cp.pcmCruise or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
len(cp.safetyConfigs) not in (1, 2)):
len(cp.safetyConfigs) not in (1, 2) or (cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1)):
return False
flags, word = profile
if (int(cp.flags) & ~int(FordFlags.HAS_BSM)) != flags:
@@ -32,7 +31,7 @@ def qualified(cp):
cp.safetyConfigs[0].safetyParam != 0):
return False
safety = cp.safetyConfigs[-1]
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == word + int(cp.openpilotLongitudinalControl)
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == word + int(cp.openpilotLongitudinalControl)
class NewPortCurvatureController(FordLateralController):
@@ -1,7 +1,6 @@
"""Ford physical cancel/resume switch context; no new cruise authority."""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.ford.classic_lateral import CLASSIC_EXTENDED_CARS
from opendbc.car.ford.generic_canfd_lateral import qualified as generic_canfd_qualified
@@ -31,6 +30,8 @@ def qualified(CP) -> bool:
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
if angle_qualified(CP):
return not CP.openpilotLongitudinalControl
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
"""Existing stock profiles with the paired physical-driver button contract."""
if (CP.brand != "ford" or CP.carFingerprint not in tuple(CAR) or CP.openpilotLongitudinalControl or CP.alternativeExperience not in (0, 32) or
CP.passive or CP.dashcamOnly or CP.notCar or len(CP.safetyConfigs) not in (1, 2) or
@@ -40,17 +41,17 @@ def qualified(CP) -> bool:
CP.safetyConfigs[0].safetyParam != 0):
return False
safety = CP.safetyConfigs[-1]
if safety.safetyModel != structs.CarParams.SafetyModel.ford or base_safety_param(CP) not in (2, 8, 10, 12, 18, 32, 66):
if safety.safetyModel != structs.CarParams.SafetyModel.ford or safety.safetyParam not in (2, 8, 10, 12, 18, 32, 66):
return False
if safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == 32:
if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == 32:
return CP.carFingerprint in CLASSIC_EXTENDED_CARS and not CP.flags & ~int(FordFlags.HAS_BSM)
if base_safety_param(CP) == 66:
if safety.safetyParam == 66:
return generic_canfd_qualified(CP)
expected = (int(FordSafetyFlags.CANFD) if CP.flags & FordFlags.CANFD else 0)
expected |= int(FordSafetyFlags.NEW_PORT) if CP.flags & FordFlags.NEW_PORT else 0
expected |= int(FordSafetyFlags.LKA_STEERING) if CP.flags & FordFlags.LKA_STEERING else 0
if base_safety_param(CP) == 18:
if safety.safetyParam == 18:
if CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or expected != int(FordSafetyFlags.CANFD):
return False
expected |= int(FordSafetyFlags.MACH_E_EXTENDED)
return base_safety_param(CP) == expected
return safety.safetyParam == expected
@@ -20,24 +20,18 @@ PROFILES = (
@pytest.mark.parametrize('identity,base_word', PROFILES)
@pytest.mark.parametrize('alpha', (False, True))
@pytest.mark.parametrize('topology', ('automatic', 'manual', 'multi-panda', 'manual-multi'))
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, topology, monkeypatch):
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, monkeypatch):
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
from opendbc.car.ford.tests.test_three_ports import params
monkeypatch.setenv('REPLAY', '1')
monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0')
offset = 4 if topology in ('multi-panda', 'manual-multi') else 0
manual = topology in ('manual', 'manual-multi')
fp = gen_empty_fingerprint()
if not manual:
fp[offset][0x5A] = 8
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, fp, [], alpha, True, False)
cp = params(identity, alpha, False)
assert cp.alternativeExperience == 0
assert cp.safetyConfigs[-1].safetyParam == base_word | int(alpha)
assert cp.safetyConfigs[0].safetyParam == base_word | int(alpha)
with OpenpilotPrefix():
saved = Params()
saved.put_bool('AlwaysOnLateral', False, block=True)
@@ -73,17 +67,15 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
if topology != "manual":
fingerprint[offset][0x5A] = 8
fingerprint[offset + 2].update({0x3D6: 8, 0x186: 8})
expected_long = alpha
initial_word = base_word | int(expected_long)
expected_word = initial_word | (256 if topology == "manual" else 0)
expected_long = alpha and not release
expected_word = base_word | int(expected_long)
initial = CarInterface.get_params(identity, fingerprint, [], expected_long, release, False)
initial.carVin = "0" * 17
initial.carFw = []
admitted = True
admitted = topology == "automatic"
expected = initial.to_dict()
expected["alternativeExperience"] = 32 if admitted else 0
expected["safetyConfigs"][-1]["safetyParam"] = expected_word
assert initial.safetyConfigs[-1].safetyParam == initial_word
assert initial.safetyConfigs[-1].safetyParam == expected_word
assert initial.openpilotLongitudinalControl == expected_long
assert not initial.passive and not initial.dashcamOnly
with OpenpilotPrefix():
@@ -153,7 +145,7 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
controls.publish(command, lateral_log)
ci.apply(command.as_reader(), 1_000_000_000)
if admitted:
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, PandaSlot, SafetyState, encode_safety
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, SafetyState, encode_safety
for tick, (acknowledged, stale) in enumerate(((False, False), (True, False), (True, True)), 1):
now = 1_000_000_000 + tick * 10_000_000
@@ -168,11 +160,8 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
axis.aolAxisState.nativeAcknowledged = acknowledged
native = messaging.new_message(SAFETY_SERVICE, 0, valid=True, logMonoTime=now)
native.aolSafetyWire = encode_safety(SafetyState(
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[-1].safetyModel.raw),
card.CP.safetyConfigs[-1].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current', sourcePandaStatesMonoTime=now,
pandaInventory=tuple(PandaSlot(index, 'fixture-panda' if index == len(card.CP.safetyConfigs) - 1 else 'passive-panda',
int(config.safetyModel.raw), config.safetyParam, 32, False, False)
for index, config in enumerate(card.CP.safetyConfigs))))
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[0].safetyModel.raw),
card.CP.safetyConfigs[0].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current'))
controls.sm.update_msgs(now / 1e9, [axis.as_reader(), native.as_reader()])
active_command, _ = controls.state_control()
assert active_command.enabled
@@ -193,7 +182,7 @@ def test_actual_card_aol_publication_and_disabled_controls(identity, word, alpha
@pytest.mark.parametrize("identity,word", PROFILES)
@pytest.mark.parametrize("topology", ("manual", "multi-panda"))
def test_aol_manual_and_multi_panda_keep_actual_ordinary_startup(identity, word, topology, monkeypatch):
def test_aol_denied_topology_preserves_actual_ordinary_startup(identity, word, topology, monkeypatch):
run_startup(identity, word, False, False, topology, monkeypatch)
@@ -17,7 +17,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.card import Car, alpha_long_requested
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.controller_extensions import ManualTurnInputs
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
@@ -34,7 +34,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
saved.put_bool('FordHumanTurnDetection', False, block=True)
for key, value in (("FordCurvatureBlendLow", 0.25), ("FordCurvatureBlendHigh", 0.65), ("FordCurvatureLaneChangeFactor", 1.1)):
saved.put(key, value, block=True)
cp = params(identity, alpha, release)
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
cp.carVin = '0' * 17
cp.carFw = []
expected = cp.to_dict()
@@ -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,83 +0,0 @@
"""Dom CAN-FD cluster scope through actual factory, setting UI and controller."""
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
from opendbc.car.ford.values import CAR, FordFlags
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.car.ford.cluster_preferences import KEY, qualified
from openpilot.starpilot.controller_extensions import configure_controller
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import row_change
DOM_CANFD = (
CAR.FORD_MUSTANG_MACH_E_MK1, CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4,
CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2, CAR.FORD_MONDEO_MK5,
)
def test_scope_covers_every_dom_canfd_profile():
assert set(DOM_CANFD) == {car for car in CAR if car.config.flags & FordFlags.CANFD and not car.config.flags & FordFlags.LKA_STEERING}
@pytest.mark.parametrize('identity', DOM_CANFD)
@pytest.mark.parametrize('aol', [False, True])
@pytest.mark.parametrize('enabled', [False, True])
def test_all_dom_profiles_have_shared_setting_and_correct_active_status(identity, aol, enabled):
with OpenpilotPrefix():
saved = Params()
controller, state = setup(aol=aol, identity=identity)
assert qualified(controller.CP)
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(identity))
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.available and row.value == 'Off'
if enabled:
assert owner.apply(row_change(row, 1))
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input.enabled == enabled
command = structs.CarControl()
for _ in range(10):
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
if identity == CAR.FORD_MUSTANG_MACH_E_MK1 and aol:
assert not UI.intersection(address for address, _, _ in frames)
command.latActive = True
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
statuses = []
for _ in range(110):
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
for frame in frames:
if frame[0] == 0x18A:
parser.update([(controller.frame * 10_000_000, [frame])])
statuses.append(parser.vl['ACCDATA_3']['Tja_D_Stat'])
assert statuses and set(statuses) == {7 if enabled else 2}
assert controller.cluster_extended_announced
@pytest.mark.parametrize('identity', [car for car in CAR if car not in DOM_CANFD])
def test_non_canfd_fords_do_not_show_cluster_setting(identity):
with OpenpilotPrefix():
saved = Params()
controller, _ = setup(identity=identity)
assert not qualified(controller.CP)
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input is None
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(identity))
assert KEY not in {r.key for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows}
def test_invalid_profile_does_not_enable_display_preference():
with OpenpilotPrefix():
saved = Params()
Path(saved.get_param_path(KEY)).write_bytes(b'1')
controller, _ = setup()
controller.CP.safetyConfigs[0].safetyParam = 0
assert not qualified(controller.CP)
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input is None
@@ -10,14 +10,14 @@ from opendbc.car.ford.values import CAR, FordFlags
@pytest.mark.parametrize("car", sorted(GENERIC_CANFD_CARS))
@pytest.mark.parametrize("alpha,release", ((False,False),(True,False),(False,True),(True,True)))
def test_actual_profile_and_release_long_support(car, alpha, release):
def test_actual_profile_and_release_long_denial(car, alpha, release):
fp = gen_empty_fingerprint()
fp[0][0x5A] = 8
fp[2].update({0x3D6:8,0x186:8})
cp = CarInterface.get_params(car, fp, [], alpha, release, False)
assert cp.alphaLongitudinalAvailable
assert cp.openpilotLongitudinalControl == alpha
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha else 66)
assert cp.alphaLongitudinalAvailable == (not release)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha and not release else 66)
assert cp.steerActuatorDelay == pytest.approx(.22)
assert qualified(cp)
@@ -212,8 +212,8 @@ def test_mache_camera_source_preserves_exact_factory_words(alpha, release, bsm):
if bsm:
fp[0].update({0x3A6: 8, 0x3A7: 8})
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fp, [], alpha, release, False)
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha else 18)
assert cp.openpilotLongitudinalControl == alpha
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha and not release else 18)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert not cp.radarUnavailable
radar = RadarInterface(cp)
assert radar.rcp.bus == 2 and radar.generic_canfd_lead
@@ -14,15 +14,15 @@ def fingerprint(offset=0, secoc=False):
class TestMachEProfile(unittest.TestCase):
def test_actual_params_stock_debug_long_and_release_long(self):
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 19)):
def test_actual_params_stock_debug_long_and_release_stock(self):
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 18)):
with self.subTest(alpha=alpha, release=release):
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(), [], alpha, release, False)
self.assertEqual(cp.safetyConfigs[-1].safetyParam, word)
self.assertEqual(cp.openpilotLongitudinalControl, word == 19)
self.assertEqual(cp.alternativeExperience, 0)
if release:
self.assertTrue(cp.alphaLongitudinalAvailable)
self.assertFalse(cp.alphaLongitudinalAvailable)
def test_secoc_unrelated_flags_and_ae_do_not_select_extension(self):
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(secoc=True), [], False, False, False)
@@ -1,113 +0,0 @@
"""Mach-E display preference, warning priority, and ownership regressions."""
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.ford import fordcan
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
from opendbc.car.ford.values import CAR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.car.ford.cluster_preferences import KEY, FordClusterPreference, read_choice
from openpilot.starpilot.controller_extensions import configure_controller
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import row_change
def status(owner, state, command, parser):
for _ in range(30):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
for address, data, bus in frames:
if address == 0x18A:
parser.update([(owner.frame * 10_000_000, [(address, data, bus)])])
return parser.vl['ACCDATA_3']
raise AssertionError('No active cluster message')
def test_saved_opt_in_through_actual_controller_configuration_and_warning_priority():
with OpenpilotPrefix():
saved = Params()
owner, state = setup()
configure_controller(SimpleNamespace(CP=owner.CP, CC=owner), saved)
assert isinstance(owner.hands_free_cluster_input, FordClusterPreference)
assert not owner.hands_free_cluster_input.enabled
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
command = structs.CarControl(latActive=True)
state.acc_tja_status_stock_values['FcwAudioWarn_B_Rq'] = 1
assert status(owner, state, command, parser)['Tja_D_Stat'] == 2
Path(saved.get_param_path(KEY)).write_bytes(b'1')
for _ in range(101):
owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert status(owner, state, command, parser)['Tja_D_Stat'] == 7
command.hudControl.leftLaneDepart = True
assert status(owner, state, command, parser)['Tja_D_Stat'] == 3
command.hudControl.leftLaneDepart = False
command.hudControl.rightLaneDepart = True
assert status(owner, state, command, parser)['Tja_D_Stat'] == 4
command.hudControl.rightLaneDepart = False
fields = status(owner, state, command, parser)
assert fields['FcwAudioWarn_B_Rq'] == 1
command.hudControl.visualAlert = structs.CarControl.HUDControl.VisualAlert.fcw
assert status(owner, state, command, parser)['FcwVisblWarn_B_Rq'] == 1
command.latActive = False
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert not UI.intersection(address for address, _, _ in frames)
Path(saved.get_param_path(KEY)).write_bytes(b'0')
command.latActive = True
for _ in range(101):
owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert status(owner, state, command, parser)['Tja_D_Stat'] == 2
@pytest.mark.parametrize('raw', [None, b'0', b'1', b'yes', b'1\n', b'', b'1' * 20])
def test_preference_only_accepts_exact_dom_opt_in(raw):
with OpenpilotPrefix():
saved = Params()
if raw is not None:
Path(saved.get_param_path(KEY)).write_bytes(raw)
enabled, observed, _, _ = read_choice(saved)
assert enabled == (raw == b'1')
assert observed == (b'' if raw is not None and len(raw) > 8 else raw)
@pytest.mark.parametrize('enabled,main,left,right,expected', [
(True, True, False, False, 7), (True, True, True, False, 3), (True, True, False, True, 4),
(True, True, True, True, 3), (False, True, False, False, 1),
(False, True, True, False, 5), (False, True, False, True, 6), (False, False, False, False, 0),
])
def test_hands_free_flag_does_not_override_departure_standby_or_off(enabled, main, left, right, expected):
owner, state = setup()
hud = structs.CarControl.HUDControl(leftLaneDepart=left, rightLaneDepart=right)
frame = fordcan.create_acc_ui_msg(owner.packer, owner.CAN, owner.CP, main, enabled, False, False, False,
hud, state.acc_tja_status_stock_values, True)
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
parser.update([(1, [frame])])
assert parser.vl['ACCDATA_3']['Tja_D_Stat'] == expected
def test_shared_feature_ui_saves_opt_in_and_rejects_onroad_stale_or_wrong_vehicle():
with OpenpilotPrefix():
saved = Params()
controller, _ = setup()
parked = [True]
owner = FeatureSettingsOwner(saved, lambda group: parked[0],
vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(controller.CP.carFingerprint))
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.value == 'Off' and row.available
request = row_change(row, 1)
assert request is not None
parked[0] = False
assert not owner.apply(request)
parked[0] = True
assert not owner.apply(replace(request, vehicle_fingerprint=str(CAR.FORD_F_150_MK14)))
assert owner.apply(request)
assert read_choice(saved)[0] == 1
assert not owner.apply(request)
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.value == 'On'
@@ -1,184 +0,0 @@
"""Recorded Mach-E turn inputs retain Dom display state across steering handoff."""
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.carstate import CarState
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR, DBC
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests import test_ford_aol
from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.car.ford.cluster_preferences import FordClusterPreference, KEY
class RecordedPreview:
def __init__(self):
self.model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.] * 33),
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
def update(self):
pass
def apply_blend_settings(self, controller):
lateral = controller.mache_lateral or controller.classic_lateral
lateral.set_blend_settings(.4, .4, .85)
def lateral_snapshot(self, speed):
return self.model, ModelConstants.T_IDXS, .2, True
def assist_permission(self):
return (), False
def run_turn(*, hands_free, angle, torque, desired, speed, yaw, identity=CAR.FORD_MUSTANG_MACH_E_MK1):
native = test_ford_aol.TestFordAolDriverIntent()
native.setUp()
native.reset(18 if identity == CAR.FORD_MUSTANG_MACH_E_MK1 else 66)
native.arm()
with OpenpilotPrefix():
saved = Params()
Path(saved.get_param_path(KEY)).write_bytes(b"1" if hands_free else b"0")
fingerprint = gen_empty_fingerprint()
fingerprint[0].update({0x5A: 8, 0x3A6: 8, 0x3A7: 8})
fingerprint[2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, 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.flags == 3
controller = CarController(DBC[cp.carFingerprint], cp)
controller.hands_free_cluster_input = FordClusterPreference(saved)
controller.manual_turn_inputs = RecordedPreview()
state = CarState(cp)
state.update(state.get_can_parsers(cp))
cs = structs.CarState(canValid=True, vEgo=speed, vEgoRaw=speed, yawRate=yaw,
gearShifter=structs.CarState.GearShifter.drive)
cs.cruiseState.available = True
command = structs.CarControl(latActive=False)
command.actuators.curvature = desired
state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] = 1
state.acc_tja_status_stock_values['FcwAudioWarn_B_Rq'] = 1
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
steering = []
displays = []
def step():
native.pump(1, mask=int(command.latActive), speed=speed, curvature=yaw / speed)
state.out = cs.as_reader()
_, frames = controller.update(command.as_reader(), state, (1_000_000 + native.tick * 10_000) * 1000)
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:
steering.append(bytes(data))
if address == 0x18A:
parser.update([(native.tick * 10_000_000, [(address, data, bus)])])
displays.append(dict(parser.vl['ACCDATA_3']))
return frames
# Startup remains stock until an active replacement is actually accepted.
for _ in range(25):
assert not {address for address, _, _ in step()} & {0x18A, 0x3D8}
assert native.safety.safety_fwd_hook(2, 0x18A) == 0
native.arm()
command.latActive = True
for _ in range(60):
step()
assert displays and displays[-1]['Tja_D_Stat'] == (7 if hands_free else 2)
assert native.safety.aol_get_permission_mask() == 1
assert native.safety.safety_fwd_hook(2, 0x18A) == -1
# Both original onset tuples have driver input, a right signal and >12deg.
cs.steeringPressed = True
cs.steeringAngleDeg = angle
cs.steeringTorque = torque
cs.rightBlinker = True
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()
if any(address == 0x3D6 for address, _, _ in frames):
assert ((steering[-1][0] >> 4) & 7) == 0
assert native.safety.safety_fwd_hook(2, 0x3D6) == 0
assert native.safety.safety_fwd_hook(2, 0x18A) == -1
assert native.safety.safety_fwd_hook(2, 0x3D8) == -1
assert native.safety.aol_get_permission_mask() == 1
assert len(displays) > count
for display in displays[count:]:
assert display['Tja_D_Stat'] == (7 if hands_free else 2)
assert display['FcwVisblWarn_B_Rq'] == 1
assert display['FcwAudioWarn_B_Rq'] == 1
cs.steeringPressed = False
cs.steeringAngleDeg = 0.
cs.steeringTorque = 0.
cs.rightBlinker = False
for _ in range(50):
step()
assert ((steering[-1][0] >> 4) & 7) == 1
assert displays[-1]['Tja_D_Stat'] == (7 if hands_free else 2)
command.latActive = False
for _ in range(25):
assert not {address for address, _, _ in step()} & {0x18A, 0x3D8}
assert native.safety.safety_fwd_hook(2, 0x18A) == 0
assert native.safety.safety_fwd_hook(2, 0x3D8) == 0
assert native.safety.aol_get_permission_mask() == 0
return steering
@pytest.mark.parametrize('angle,torque,desired,speed,yaw', [
(-12.2, -1.6875, .000041086183, 2.55, -.006),
(-38.7, -2., -.000324735534, 2.563888788, -.033),
])
def test_recorded_turn_keeps_dom_display_and_hands_free_does_not_change_steering(angle, torque, desired, speed, yaw):
hands_free = run_turn(hands_free=True, angle=angle, torque=torque, desired=desired, speed=speed, yaw=yaw)
plain = run_turn(hands_free=False, angle=angle, torque=torque, desired=desired, speed=speed, yaw=yaw)
assert plain == hands_free
@pytest.mark.parametrize('saved', [False, True])
@pytest.mark.parametrize('flag', [ParamKeyFlag.CLEAR_ON_MANAGER_START, ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION,
ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION, ParamKeyFlag.CLEAR_ON_IGNITION_ON])
def test_cluster_preference_survives_native_cleanup(tmp_path, saved, flag):
params = Params(str(tmp_path))
key = 'FordHandsFreeCluster'
assert params.check_key(key) and params.get_type(key) == ParamKeyType.BOOL
params.put(key, saved, block=True)
sentinel = Path(params.get_param_path('FordUnregisteredSentinel'))
sentinel.write_bytes(b'1')
params.clear_all(flag)
assert params.get(key) == saved and not sentinel.exists()
reopened = Params(str(tmp_path))
assert reopened.get(key) == saved
assert Path(reopened.get_param_path(key)).read_bytes() == (b'1' if saved else b'0')
assert FordClusterPreference(reopened).enabled == saved
@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,60 +0,0 @@
"""Mach-E AOL must not inject UI while native safety forwards stock UI."""
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.carstate import CarState
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR, DBC
from openpilot.common.prefix import OpenpilotPrefix
UI = {0x3D8, 0x18A}
def setup(*, aol=True, identity=CAR.FORD_MUSTANG_MACH_E_MK1):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x5A] = 8
fingerprint[2].update({0x3D6: 8, 0x186: 8})
with OpenpilotPrefix():
cp = CarInterface.get_params(identity, fingerprint, [], False, False, False)
controller = CarController(DBC[cp.carFingerprint], cp)
# Card finalizes AOL qualification after the controller is constructed.
cp.alternativeExperience = 32 if aol else 0
state = CarState(cp)
state.update(state.get_can_parsers(cp))
cs = structs.CarState(canValid=True, vEgo=15., vEgoRaw=15., gearShifter=structs.CarState.GearShifter.drive)
cs.cruiseState.available = True
state.out = cs.as_reader()
return controller, state
def test_mache_idle_leaves_stock_display_and_fcw_untouched_then_owns_active_ui():
owner, state = setup()
command = structs.CarControl()
# Real stock UI/FCW fields remain available to be forwarded by native safety.
state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] = 1
state.acc_tja_status_stock_values['Tja_D_Stat'] = 1
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert not UI.intersection(address for address, _, _ in frames)
command.latActive = True
active_ui = set()
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
active_ui.update(address for address, _, _ in frames if address in UI)
assert active_ui == UI
command.latActive = False
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert not UI.intersection(address for address, _, _ in frames)
assert state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] == 1
@pytest.mark.parametrize('identity,aol,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):
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
@@ -28,7 +28,7 @@ class TestNewPortCurvature(unittest.TestCase):
for identity in PROFILES:
for alpha, release in ((False, False), (True, False), (False, True), (True, True)):
cp = factory(identity, alpha, release)
expected_long = bool(alpha)
expected_long = bool(alpha and (identity == CAR.FORD_EDGE_MK2 or not release))
self.assertTrue(cp.pcmCruise)
self.assertEqual(cp.openpilotLongitudinalControl, expected_long)
self.assertEqual(cp.safetyConfigs[-1].safetyParam, (8 if identity == CAR.FORD_EDGE_MK2 else 10) + int(expected_long))
@@ -156,7 +156,6 @@ class TestNewPortCurvature(unittest.TestCase):
command, controller_log = controls.state_control()
self.assertTrue(command.enabled)
control_events = []
def capture_control(service, event, collector=control_events):
if service == "carControl":
collector.append(messaging.log_from_bytes(event.to_bytes()))
@@ -20,7 +20,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.card import Car, alpha_long_requested
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.controller_extensions import ManualTurnInputs
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
@@ -36,7 +36,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
saved.put_bool('IsReleaseBranch', release, block=True)
saved.put_bool('DisableOpenpilotLongitudinal', disable, block=True)
saved.put_bool('FordHumanTurnDetection', False, block=True)
cp = params(identity, alpha, release)
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
cp.carVin = '0' * 17
cp.carFw = []
expected = cp.to_dict()
@@ -1,6 +1,4 @@
"""Original switch context, state event and reached controller regressions."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs, gen_empty_fingerprint
from opendbc.car.ford.interface import CarInterface
@@ -41,27 +39,6 @@ def test_actual_parser_cancel_edges_are_op_long_only_and_contextual():
assert cancel == (expected if op_long else [])
@pytest.mark.parametrize('vehicle', [CAR.FORD_ESCAPE_MK4, CAR.FORD_MUSTANG_MACH_E_MK1])
@pytest.mark.parametrize('op_long', [False, True])
@pytest.mark.parametrize('signal,button', [
('CcAslButtnSetIncPress', 'accelCruise'), ('CcAslButtnResIncPress', 'accelCruise'),
('CcAslButtnIndxIncPress', 'accelCruise'), ('CcAslButtnSetDecPress', 'decelCruise'),
('CcAslButtnResDecPress', 'decelCruise'), ('CcAslButtnIndxDecPress', 'decelCruise'),
])
def test_directional_cruise_edges_reach_software_cruise_only_with_op_long(vehicle, op_long, signal, button):
cp = params(vehicle, alpha=op_long)
state = CarState(cp)
parsers = state.get_can_parsers(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for i, (pressed, expected) in enumerate(((0, []), (1, [True]), (1, []), (0, [False]))):
parsers[Bus.pt].update([(1_000_000_000 + i * 10_000_000, [
packer.make_can_msg('Steering_Data_FD1', parsers[Bus.pt].bus, {signal: pressed}),
])])
out = state.update(parsers)
edges = [event.pressed for event in out.buttonEvents if event.type == getattr(structs.CarState.ButtonEvent.Type, button)]
assert edges == (expected if op_long else [])
def test_reached_controller_preserves_precedence_cadence_and_stock_bits():
cp = params(CAR.FORD_MUSTANG_MACH_E_MK1, alpha=False)
controller = CarController(DBC[cp.carFingerprint], cp)
@@ -62,7 +62,7 @@ class TestFordThreePorts(unittest.TestCase):
self.assertTrue(requested.openpilotLongitudinalControl)
self.assertTrue(requested.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
if candidate == CAR.FORD_MONDEO_MK5:
self.assertTrue(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
self.assertFalse(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
self.assertAlmostEqual(params(CAR.FORD_MUSTANG_MACH_E_MK1).steerActuatorDelay, 0.22, places=7)
self.assertFalse(params(CAR.FORD_BRONCO_SPORT_MK1).openpilotLongitudinalControl)
self.assertTrue(params(CAR.FORD_BRONCO_SPORT_MK1, alpha=True).openpilotLongitudinalControl)
@@ -1,39 +0,0 @@
"""Transit CAN angle quantization must stay within the native encoded envelope."""
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.ford.fordcan import CanBus, create_lka_msg
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent as _Native
@pytest.mark.parametrize('word', (12, 13))
@pytest.mark.parametrize('sign,endpoint', ((1, 4072), (-1, 24)))
def test_saturated_angle_with_physical_cruise_stays_inside_native_envelope(sign, endpoint, word):
native = _Native(methodName='runTest')
native.setUp()
try:
native.reset(word)
native.pump(40, main=3)
native.pump(10, main=4, mask=1)
assert native.safety.get_controls_allowed()
for angle in (1., 2., 3., 4., 5., 5.8, 6.):
native.pump(3, main=4, mask=1)
address, data, bus = create_lka_msg(native.packer, CanBus(fingerprint=gen_empty_fingerprint()), True, sign * angle,
2 if sign > 0 else 4, 0., transit=True)
raw_angle = ((data[2] & 15) << 8) | data[3]
assert 24 <= raw_angle <= 4072
if angle >= 5.8:
assert raw_angle == endpoint
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
# The adjacent encoded values remain prohibited by unchanged native safety.
rejected_angle = 4073 if sign > 0 else 23
rejected = bytearray(data)
rejected[2] = (rejected[2] & 0xF0) | (rejected_angle >> 8)
rejected[3] = rejected_angle & 0xFF
assert not native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, rejected))
address, data, bus = create_lka_msg(native.packer, CanBus(fingerprint=gen_empty_fingerprint()), False, sign * 6., transit=True)
assert data[0] >> 5 == 0
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
finally:
native.tearDown()
+3 -4
View File
@@ -48,7 +48,6 @@ class FordSafetyFlags(IntFlag):
MACH_E_EXTENDED = 16
CLASSIC_EXTENDED = 32
EXPLORER_EXTENDED = CLASSIC_EXTENDED # Existing public profile spelling.
MANUAL = 256
BLUEPILOT_ANGLE = 128
GENERIC_CANFD_EXTENDED = 64
@@ -329,12 +328,12 @@ FW_QUERY_CONFIG = FwQueryConfig(
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
logging=True,
),
*[Request(
Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
bus=bus,
) for bus in (0, 4)],
bus=0,
),
*[Request(
[StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)],
[StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)],
+2 -29
View File
@@ -1,9 +1,6 @@
"""Independent lateral axes for exact source-qualified Bolt and Volt owners."""
import math
from functools import lru_cache
from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader
from opendbc.car.gm.cancel_remap import aol_enabled, valid_alternative_experience
from opendbc.car.structs import CarParams
@@ -38,11 +35,11 @@ GM_AOL_WORDS = (frozenset(range(0xE600, 0xE608)) | frozenset(range(0xE610, 0xE61
0xC1D1, 0xC1D3,
0xC110, 0xC111, 0xC120, 0xC121, 0xC130, 0xC131, 0xC140, 0xC141,
0xE800, 0xE801, 0xE802, 0xE803, 0xE804, 0xE805,
0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC163, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187,
0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187,
))
def _qualified_gm_uncached(cp) -> bool:
def qualified_gm(cp) -> bool:
try:
if (cp.brand != 'gm' or cp.notCar or cp.passive or cp.dashcamOnly or
cp.steerControlType != CarParams.SteerControlType.torque or
@@ -97,30 +94,6 @@ def _qualified_gm_uncached(cp) -> bool:
return False
# Qualification depends only on the exact finalized CarParams value. Cache no
# live vehicle state, native grant, source timestamp, or output eligibility.
@lru_cache(maxsize=8)
def _qualified_gm_bytes(key: bytes) -> bool:
with CarParams.from_bytes(key) as cp:
return _qualified_gm_uncached(cp)
def qualified_gm(cp) -> bool:
cp_type = type(cp)
if cp_type is _DynamicStructBuilder or cp_type is _DynamicStructReader:
try:
if cp.schema == CarParams.schema:
if cp.brand != 'gm':
return False
snapshot = cp.copy() if cp_type is _DynamicStructBuilder else cp.as_builder()
key = snapshot.to_bytes()
if len(key) <= 64 * 1024:
return _qualified_gm_bytes(key)
except Exception:
pass
return _qualified_gm_uncached(cp)
def lateral_request(cp, cc) -> bool:
"""Only a finalized independent-axis owner may relax its cruise-active lateral gate."""
return (qualified_gm(cp) and aol_enabled(cp) and cc.latActive)
@@ -1,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)
+10 -29
View File
@@ -1,4 +1,4 @@
"""Mode-transition output shaping for admitted GM longitudinal owners."""
"""Mode-transition output shaping for admitted Bolt longitudinal owners."""
class BoltModeTransition:
@@ -37,7 +37,7 @@ class BoltModeTransition:
if output < 0.0 and output < last_output:
progress = min(1.0, self.timer / self.duration)
urgency = abs(output / -4.0)
urgency_smooth = min(1.0, urgency**0.4)
urgency_smooth = min(1.0, urgency ** 0.4)
blend = 1.0 - (1.0 - progress) * (1.0 - urgency_smooth)
output = last_output + (output - last_output) * blend
return output
@@ -45,32 +45,16 @@ class BoltModeTransition:
def policy_for(cp):
from opendbc.car.structs import CarParams
from opendbc.car.gm.values import (
CAR,
GMFlags,
GMSafetyFlags,
is_bolt_cc_profile,
is_bolt_euv_longitudinal,
is_ordinary_cc_profile,
is_ordinary_camera_profile,
is_conventional_cc_pedal_profile,
is_volt_longitudinal,
)
from opendbc.car.gm.values import (CAR, GMFlags, GMSafetyFlags, is_bolt_cc_profile, is_bolt_euv_longitudinal,
is_ordinary_cc_profile, is_ordinary_camera_profile, is_conventional_cc_pedal_profile)
from opendbc.car.gm.longitudinal import policy_for as pedal_policy_for
if not cp.openpilotLongitudinalControl:
return None
if (
is_volt_longitudinal(cp)
or is_conventional_cc_pedal_profile(cp)
or is_ordinary_camera_profile(cp, longitudinal=True)
or is_ordinary_cc_profile(cp)
or is_bolt_cc_profile(cp)
or is_bolt_euv_longitudinal(cp)
):
if (is_conventional_cc_pedal_profile(cp) or is_ordinary_camera_profile(cp, longitudinal=True) or
is_ordinary_cc_profile(cp) or is_bolt_cc_profile(cp) or is_bolt_euv_longitudinal(cp)):
return BoltModeTransition()
from opendbc.car.gm.values import gm_control_word
base = GMSafetyFlags.HW_CAM | GMSafetyFlags.EV | GMSafetyFlags.PEDAL_LONG | GMSafetyFlags.PADDLE_SCHED
words = {
CAR.CHEVROLET_BOLT_CC_2017: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017,
@@ -78,12 +62,9 @@ def policy_for(cp):
CAR.CHEVROLET_BOLT_CC_2022_2023: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_GEN2,
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL: base | GMSafetyFlags.BOLT_ACC_PEDAL | GMSafetyFlags.BOLT_GEN2,
}
if (
pedal_policy_for(cp) is not None
and not cp.flags & GMFlags.CC_LONG.value
and len(cp.safetyConfigs) == 1
and cp.networkLocation == CarParams.NetworkLocation.fwdCamera
and gm_control_word(cp) == words.get(cp.carFingerprint)
):
if (pedal_policy_for(cp) is not None and not cp.flags & GMFlags.CC_LONG.value and
len(cp.safetyConfigs) == 1 and
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and
gm_control_word(cp) == words.get(cp.carFingerprint)):
return BoltModeTransition()
return None
+7 -41
View File
@@ -1,5 +1,4 @@
from opendbc.car.gm.one_pedal import VoltOnePedal
from opendbc.car.gm.steering_authority import EPS_STATUS_TIMEOUT_NS
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal, camera_acc_pedal_profile, volt_cc_pedal_profile
from opendbc.car.gm.longitudinal import volt_sng_release
from opendbc.car.gm.auto_hold import AutoHold, config_for as auto_hold_config_for, hold_brake as estimate_hold_brake
@@ -7,7 +6,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 +265,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 +299,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)
@@ -580,15 +575,7 @@ class CarController(CarControllerBase):
if self.CP.openpilotLongitudinalControl:
stock_steer_ready = (stock_steer_ready and CS.pedal_sensor_healthy and
0 < CS.pedal_sensor_ts_nanos <= now_nanos <= CS.pedal_sensor_ts_nanos + PEDAL_SENSOR_TIMEOUT_NS)
# An SDGM EPS can report inactive while the driver holds the brake. Emit
# neutral for that measured pause so releasing the brake starts a new request.
# Keep the existing acquisition deadline and genuine refusal latch intact.
sdgm_brake_pause = False
if self.volt_sdgm_long and CS.out.brakePressed:
steering = CS.steering_authority
sdgm_brake_pause = (steering.enabled and steering.status == 0 and
0 < steering.status_ns <= now_nanos <= steering.status_ns + EPS_STATUS_TIMEOUT_NS)
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched and not sdgm_brake_pause
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched
# Steering (Active: 50Hz, inactive: 10Hz)
steer_step = self.params.STEER_STEP if lat_active else self.params.INACTIVE_STEER_STEP
@@ -605,11 +592,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 +845,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 +864,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.
@@ -1234,7 +1200,7 @@ class CarController(CarControllerBase):
self.bolt_removed_cancel_credit_used = CS.conventional_cancel_credit.credit_ns
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, CruiseButtons.CANCEL))
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.bolt_cc_profile and not self.volt_camera_removed:
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.bolt_cc_profile:
# Silence "Take Steering" alert sent by camera, forward PSCMStatus with HandsOffSWlDetectionStatus=1
if self.frame % 10 == 0:
can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status))
+3 -24
View File
@@ -17,8 +17,7 @@ from opendbc.car.gm.conventional_pedal import CancelCredit
from opendbc.car.gm.values import (DBC, AccState, CruiseButtons, STEER_THRESHOLD, SDGM_CAR, ALT_ACCS,
ASCM_INTERCEPT_CAR, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, GMFlags, GMSafetyFlags, NO_ACC_BOLT_CAR,
CC_GATEWAY_STOCK_CAR, requires_camera_state_sources, is_conventional_cc_pedal_profile, is_silverado_cc_pedal_profile,
is_volt_cc_profile, is_volt_cc_f1_profile, is_silverado_cc_stock_profile, is_ordinary_cc_profile,
is_malibu_cc_f1_profile, VOLT_BSM_CAR, CAR,
is_volt_cc_profile, is_silverado_cc_stock_profile, is_ordinary_cc_profile, is_malibu_cc_f1_profile, VOLT_BSM_CAR, CAR,
is_volt_gateway_profile, is_volt_gateway_alternate_brake, is_bolt_cc_profile, BOLT_CC_WORDS,
is_bolt_pedal_profile, is_bolt_pedal_removed_profile, is_bolt_present_no_acc_pedal_profile, is_volt_camera_removed,
is_ordinary_camera_profile, is_ordinary_camera_removed)
@@ -62,7 +61,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 +74,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
@@ -272,7 +268,7 @@ class CarState(CarStateBase):
pt_cp.ts_nanos["ECMCruiseControl"]["CruiseActive"],
pt_cp.ts_nanos["ASCMSteeringButton"]["RollingCounter"],
pt_cp.ts_nanos["ECMEngineStatus"]["CruiseMainOn"],
(pt_cp.ts_nanos["EBCMBrakePedalPosition"]["BrakePedalPosition"] if is_malibu_cc_f1_profile(self.CP) or is_volt_cc_f1_profile(self.CP) else
(pt_cp.ts_nanos["EBCMBrakePedalPosition"]["BrakePedalPosition"] if is_malibu_cc_f1_profile(self.CP) else
pt_cp.ts_nanos["ECMAcceleratorPos"]["BrakePedalPos"]),
pt_cp.ts_nanos["ECMPRDNL2"]["PRNDL2"],
pt_cp.ts_nanos["AcceleratorPedal2"]["AcceleratorPedal2"])
@@ -371,7 +367,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
@@ -410,10 +405,7 @@ class CarState(CarStateBase):
source_be_brake = (gm_control_word(self.CP) &
(GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM).value and
not gm_control_word(self.CP) & GMSafetyFlags.BRAKE_C9.value)
if is_volt_cc_f1_profile(self.CP):
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 6 or
pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0)
elif is_malibu_cc_f1_profile(self.CP):
if is_malibu_cc_f1_profile(self.CP):
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 21 or
pt_cp.vl["EBCMBrakePedalPosition"]["BrakePressed"] != 0 or
pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0)
@@ -508,10 +500,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
@@ -522,9 +510,6 @@ class CarState(CarStateBase):
pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
if is_volt_camera_removed(self.CP):
# A missing camera can leave the ECU faulted; that is not engaged cruise.
ret.cruiseState.enabled &= pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.FAULTED
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
if (self.CP.carFingerprint in CC_GATEWAY_STOCK_CAR or self.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC):
# AcceleratorPedal2 CruiseState is an ACC state, not a conventional-cruise fault source.
@@ -776,9 +761,6 @@ class CarState(CarStateBase):
if CP.carFingerprint == CAR.CHEVROLET_VOLT_CC:
pt_messages.append(("EBCMRegenPaddle", 50))
if is_volt_cc_f1_profile(CP):
pt_messages = [(name, frequency) for name, frequency in pt_messages if name != "ECMAcceleratorPos"]
pt_messages.append(("EBCMBrakePedalPosition", 100))
if is_bolt_cc_profile(CP):
pt_messages += [("PSCMStatus", 10), ("ECMCruiseControl", 10), ("ASCMSteeringButton", 33),
@@ -886,9 +868,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]
+2 -18
View File
@@ -1,4 +1,4 @@
"""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
@@ -20,20 +20,4 @@ def automatic_volt_candidate(candidate, fingerprint, candidates):
any(pt.get(address) != size for address, size in required.items()) or
any(address in pt for address in (0xBE, 0x201, 0x2FF, 0x320))):
return candidate
# The recorded car has ordinary cruise, no ACC and no camera. The absent
# camera is not evidence of an ACC actuator owner. Read actual ECM cruise
# activity and retain button-only control; explicit ACC selections are above.
return CAR.CHEVROLET_VOLT_CC
# 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())
return CAR.CHEVROLET_VOLT_CAMERA
+5 -14
View File
@@ -16,7 +16,7 @@ from opendbc.car.gm.values import (volt_cc_pedal_profile, CAR, CarControllerPara
ORDINARY_CC_CAR, ORDINARY_CC_WORD, SILVERADO_CC_PEDAL_WORDS,
is_silverado_cc_pedal_profile, is_conventional_cc_pedal_profile,
CAMERA_STOCK_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CAMERA_ALPHA_CAR, camera_acc_pedal_profile,
VOLT_BSM_CAR, VOLT_CC_F1_WORD, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS,
VOLT_BSM_CAR, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS,
is_bolt_pedal_removed_profile, is_bolt_present_no_acc_pedal_profile)
from opendbc.car.gm.values import (malibu_hybrid_profile, MALIBU_HYBRID_SOURCES,
is_volt_sdgm_accepted_envelope, volt_sdgm_accepted_accel_max)
@@ -229,23 +229,14 @@ class CarInterface(CarInterfaceBase):
if candidate == CAR.CHEVROLET_VOLT_CC:
# Require the complete source layout for either automatic or manual identity.
pt = fingerprint[CanBus.POWERTRAIN]
f1 = 0xBE not in pt and pt.get(0xF1) == 6
required = {0xF1 if f1 else 0xBE: 6, 0x3d1: 8, 0xc9: 8, 0x1e1: 7, 0x1f5: 8, 0x34a: 5, 0x1c4: 8, 0xbd: 7}
if f1:
# Admit the recorded no-ACC source graph, never a broken pedal/camera graph.
required.update({0x184: 8, 0xD3: 2, 0x130: 1, 0x17D: 6, 0x210: 4, 0x236: 5})
f1_layout = (not any(address in pt for address in (0x201, 0x2FF, 0x320)) and
not any(address in fingerprint[CanBus.CAMERA] for address in (0x180, 0x320, 0x370)) and
not fingerprint[CanBus.OBSTACLE])
admitted = (not is_release and (not f1 or f1_layout) and
all(fingerprint[CanBus.POWERTRAIN].get(addr) == size for addr, size in required.items()))
required = {0xbe: 6, 0x3d1: 8, 0xc9: 8, 0x1e1: 7, 0x1f5: 8, 0x34a: 5, 0x1c4: 8, 0xbd: 7}
admitted = not is_release and all(fingerprint[CanBus.POWERTRAIN].get(addr) == size for addr, size in required.items())
ret.networkLocation = NetworkLocation.gateway
ret.radarUnavailable = True
ret.alphaLongitudinalAvailable = False
ret.openpilotLongitudinalControl = admitted
ret.pcmCruise = not admitted
ret.flags = int(GMFlags.CC_LONG | (GMFlags.NO_ACCELERATOR_POS_MSG if f1 else 0)) if admitted else 0
ret.flags = GMFlags.CC_LONG.value if admitted else 0
ret.minEnableSpeed = 24 * CV.MPH_TO_MS
ret.minSteerSpeed = 7 * CV.MPH_TO_MS
ret.longitudinalActuatorDelay = 1.0
@@ -254,7 +245,7 @@ class CarInterface(CarInterfaceBase):
if not admitted:
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
else:
ret.safetyConfigs[0].safetyParam = VOLT_CC_F1_WORD if f1 else int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)
ret.safetyConfigs[0].safetyParam = int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)
elif candidate in CC_GATEWAY_STOCK_CAR:
# Start with stock PCM ownership; qualified button-control owners override below.
@@ -1,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
# PSCMStatus is expected at 10 Hz. Three periods bound first acquisition and missing feedback.
EPS_STATUS_TIMEOUT_NS = 300_000_000
@@ -9,8 +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
camera_acc_pedal_profile(CP) is None and
return (is_volt_ascm_longitudinal(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))
@@ -46,9 +46,9 @@ def fixture(identity, removed=False, metric=False, alpha=False, present=False):
return cp, ci, CANPacker(DBC[identity][Bus.pt])
def feed(ci, packer, now, counter=0, gas=False, active=True, speed=20, stock=15, brake=False, regen=False, camera=True, eps_status=0):
def feed(ci, packer, now, counter=0, gas=False, active=True, speed=20, stock=15, brake=False, regen=False, camera=True):
vals = {
'PSCMStatus': {'LKATorqueDeliveredStatus': eps_status},
'PSCMStatus': {},
'ECMCruiseControl': {'CruiseActive': int(active), 'CruiseSetSpeed': stock * 3.6},
'ECMEngineStatus': {'CruiseMainOn': 1, 'BrakePressed': int(brake)},
'AcceleratorPedal2': {'AcceleratorPedal2': 30 if gas else 0},
@@ -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':
@@ -1054,16 +1032,13 @@ class TestBoltPaddleModes(unittest.TestCase):
for candidate in PEDAL_BOLT_CAR:
with self.subTest(candidate=candidate), OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1'}):
settings = Params()
settings.put_bool('AlwaysOnLateral', False, block=True)
card = TestGmAol.card(params(candidate, pedal=True, camera=True), settings)
ci, cp = card.CI, card.CP
self.assertIsNotNone(ci.CC.maneuver_paddle_input)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
safety = libsafety_py.libsafety
# This ordinary pedal test must not inherit the previous test's AOL policy.
safety.init_tests()
safety.set_alternative_experience(cp.alternativeExperience)
self.assertEqual(safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam), 0)
safety.init_tests()
recorder = TestGmBoltPedalSafety()
recorder.safety = safety
control = structs.CarControl()
@@ -1116,7 +1091,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
@@ -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,83 +0,0 @@
"""SDGM route26: reacquire EPS assistance after a measured brake pause."""
from types import SimpleNamespace
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
from opendbc.car.gm.values import DBC
def frame(ci, packer, tick, *, brake=False, status=1):
now = 1_000_000_000 + tick * 10_000_000
_, rows = feed_camera(SimpleNamespace(update=lambda _: None), packer, now,
counter=tick % 4, brake=brake, active=False, eps_status=status)
rows = [row for row in rows if row[0] != 0xBE and (tick % 10 == 0 or row[0] != 0x184)]
rows.append(packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 30 if brake else 0}))
cs = ci.update([(now, rows)])
control = structs.CarControl(enabled=False, latActive=True, longActive=False)
control.actuators.torque = .2
control.hudControl.setSpeed = 30.
_, sends = ci.apply(control.as_reader(), now)
steering = [data for address, data, bus in sends if address == 0x180 and bus == 0]
return cs, steering
@pytest.mark.parametrize('c9', [False, True])
@pytest.mark.parametrize('recovered', [False, True])
def test_sdgm_brake_pause_emits_neutral_then_reacquires_or_keeps_refusal(c9, recovered):
cp = sdgm_params(brake_c9=c9)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for tick in range(40):
frame(ci, packer, tick)
assert ci.CS.steering_authority.seen_active
emitted = []
for tick in range(40, 140):
cs, steering = frame(ci, packer, tick, brake=True, status=0)
emitted += steering
assert cs.brakePressed
assert not ci.CS.steering_authority.latched
assert emitted and all(not (data[0] & 8) and data[:2] in (b'\x00\x00', b'\x10\x00', b'\x20\x00', b'\x30\x00') for data in emitted)
emitted = []
for tick in range(140, 185):
cs, steering = frame(ci, packer, tick, status=1 if recovered and tick >= 150 else 0)
emitted += steering
assert any(data[0] & 8 for data in emitted)
assert ci.CS.steering_authority.latched == (not recovered)
assert cs.steerFaultTemporary == (not recovered)
if recovered:
assert ci.CS.steering_authority.seen_active
@pytest.mark.parametrize('status', [1, 2, 3])
def test_sdgm_active_feedback_and_factory_faults_are_not_brake_recovery(status):
cp = sdgm_params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
emitted = []
for tick in range(40):
cs, steering = frame(ci, packer, tick, brake=True, status=status)
emitted += steering
assert any(data[0] & 8 for data in emitted)
assert cs.steerFaultTemporary == (status == 2)
assert cs.steerFaultPermanent == (status == 3)
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('brake', [False, True])
def test_sdgm_brake_recovery_does_not_enable_unengaged_stock_control(alpha, brake):
cp = sdgm_params(alpha=alpha)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
emitted = []
for tick in range(20):
_, steering = frame(ci, packer, tick, brake=brake, status=0)
if tick >= 10:
emitted += steering
assert emitted
# The stock ACC fixture is disengaged; this recovery never enables it.
assert all(bool(data[0] & 8) == (alpha and not brake) for data in emitted)
@@ -1,44 +0,0 @@
"""Recorded SDGM route18: a steering request is not proof of EPS assistance."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests import test_ascm_intercept as ascm_fixture
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
from opendbc.car.gm.values import DBC
@pytest.mark.parametrize("c9", [False, True])
def test_sdgm_long_refused_steering_is_not_silently_active(c9):
cp = sdgm_params(alpha=True, brake_c9=c9)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
case = ascm_fixture.TestAscmIntercept(methodName="runTest")
for tick in range(40):
cs = case.authority_frame(ci, packer, tick, status=1)
for tick in range(40, 81):
cs = case.authority_frame(ci, packer, tick, status=0)
assert ci.CS.steering_authority.latched
assert cs.steerFaultTemporary
@pytest.mark.parametrize("alpha", [False, True])
@pytest.mark.parametrize("status,active,speed,driver", [(1, True, 8., 0.), (0, False, 8., 0.),
(0, True, 0., 0.), (0, True, 8., 3.), (2, True, 8., 0.), (3, True, 8., 0.)])
def test_sdgm_monitor_preserves_deliberate_pauses_and_factory_faults(alpha, status, active, speed, driver):
cp = sdgm_params(alpha=alpha)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
case = ascm_fixture.TestAscmIntercept(methodName="runTest")
for tick in range(60):
cs = case.authority_frame(ci, packer, tick, status=status, active=active and alpha, speed=speed, driver=driver)
assert not ci.CS.steering_authority.latched
assert cs.steerFaultTemporary == (status == 2)
assert cs.steerFaultPermanent == (status == 3)
@pytest.mark.parametrize("c9", [False, True])
def test_sdgm_refusal_requires_physical_disable_neutral_and_fresh_rearm(monkeypatch, c9):
# Reuse the full original parser/controller physical sequence for this exact factory CP.
monkeypatch.setattr(ascm_fixture, "params", lambda *args, **kwargs: sdgm_params(brake_c9=c9, radar=True))
ascm_fixture.TestAscmIntercept(methodName="runTest").test_volt_steering_loss_requires_driver_rearm()
@@ -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)
@@ -91,13 +91,6 @@ class TestVoltAlternateBrake(unittest.TestCase):
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for tick, expected in enumerate(baseline['tx']):
# Keep every frozen vehicle-side byte. Only the removed camera's
# obsolete PSCM replacement is intentionally absent in this topology.
if is_volt_camera_removed(cp):
self.assertIn(name, ('removed-0', 'removed-1'))
camera_status = [message for message in expected if message[0] == 0x184 and message[2] == 2]
self.assertEqual(len(camera_status), int(tick % 10 == 0))
expected = [message for message in expected if not (message[0] == 0x184 and message[2] == 2)]
now = 1_000_000_000 + tick * 10_000_000
speed = 20. if tick < 40 or tick >= 100 else 0. if tick < 80 else 6.
active, brake, gas = tick < 60 or tick >= 90, 60 <= tick < 65, 65 <= tick < 70
@@ -1,4 +1,4 @@
"""Recorded no-camera/no-ACC Volt discovery must retain distinct existing owners."""
"""Recorded camera-bypassed Volt discovery must retain distinct existing owners."""
from contextlib import ExitStack
from copy import deepcopy
import json
@@ -33,21 +33,20 @@ def discover(fingerprint):
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('release', [False, True])
def test_recorded_collision_reaches_conventional_cruise_owner(alpha, release):
def test_recorded_collision_reaches_existing_camera_removed_owner(alpha, release):
fp = recorded_fingerprint()
with patch.object(car_helpers, 'automatic_volt_candidate', side_effect=lambda candidate, *_: candidate):
old, _ = discover(fp)
assert old is None # The legacy gateway and CC pedal signatures both survive.
selected, observed = discover(fp)
assert selected == CAR.CHEVROLET_VOLT_CC and observed == fp
assert selected == CAR.CHEVROLET_VOLT_CAMERA and observed == fp
cp = CarInterface.get_params(selected, observed, [], alpha, release, False)
assert cp.dashcamOnly == release and cp.networkLocation == structs.CarParams.NetworkLocation.gateway
assert not is_volt_camera_removed(cp)
assert cp.openpilotLongitudinalControl == (not release)
assert cp.pcmCruise == release
assert cp.safetyConfigs[0].safetyParam == (0 if release else 0xC163)
if not release:
assert cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG
assert not cp.dashcamOnly and cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
assert cp.flags & GMFlags.VOLT_CAMERA_REMOVED and cp.flags & GMFlags.VOLT_CAMERA_NO_ACCEL_POS
assert is_volt_camera_removed(cp)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.pcmCruise == (not alpha or release)
assert cp.safetyConfigs[0].safetyParam == (0xC151 if alpha and not release else 0xC150)
@pytest.mark.parametrize('address,length', [(0xD3, 2), (0x130, 1), (0x17D, 6), (0x210, 4), (0x236, 5),
@@ -126,10 +125,8 @@ def test_auto_card_and_controls_use_correct_profile_but_start_with_commands_off(
fp = recorded_fingerprint()
packets = [CanData(address, bytes(size), bus) for bus, messages in fp.items() for address, size in messages.items()]
def receive(wait_for_one=False):
return [packets] if wait_for_one else []
def identify(*args, **kwargs):
return car_helpers.get_car(receive, *args[1:], **kwargs)
with OpenpilotPrefix(), ExitStack() as cleanup:
@@ -145,11 +142,11 @@ def test_auto_card_and_controls_use_correct_profile_but_start_with_commands_off(
card = Car()
cleanup.callback(card.vehicle_startup.close)
cleanup.callback(card.params._finalizer)
assert card.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC
assert card.CP.carFingerprint == CAR.CHEVROLET_VOLT_CAMERA
assert card.CP.fingerprintSource == structs.CarParams.FingerprintSource.can
assert not card.CP.dashcamOnly and not card.CP.passive
assert card.CP.safetyConfigs[0].safetyParam == 0xC163
assert card.CP.openpilotLongitudinalControl
assert card.CP.safetyConfigs[0].safetyParam == 0xC150
assert not card.CP.openpilotLongitudinalControl
controls = Controls()
cleanup.callback(controls.params._finalizer)
assert controls.CP.carFingerprint == card.CP.carFingerprint
@@ -25,17 +25,12 @@ def camera_params(alpha=True, release=False, camera_length=6, radar=False, sascm
return CarInterface.get_params(CAR.CHEVROLET_VOLT_CAMERA, fingerprint, [], alpha, release, False)
def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False, regen=False, low=False, active=True, eps_status=0):
_, messages = feed(ci, packer, now, counter=counter, speed=speed, gas=gas, brake=brake, regen=regen, eps_status=eps_status)
def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False, regen=False, low=False, active=True):
_, messages = feed(ci, packer, now, counter=counter, speed=speed, gas=gas, brake=brake, regen=regen)
messages = [message for message in messages if message[0] not in (0x1C4, 0x1F5)]
messages += [packer.make_can_msg("AcceleratorPedal2", 0, {"CruiseState": 2 if active else 0, "AcceleratorPedal2": 30 if gas else 0}),
packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 6 if low else 4}),
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
@@ -1,60 +0,0 @@
"""Route 11177: a removed camera must never receive forwarded or host traffic."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params, feed_removed
from opendbc.car.gm.tests.test_volt_camera_control import camera_params, feed_camera
from opendbc.car.gm.values import CAR, DBC
from opendbc.safety.tests.libsafety import libsafety_py
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('alternate', [False, True])
def test_removed_camera_controller_uses_only_vehicle_bus(alpha, alternate):
cp = removed_params(alpha=alpha, alternate=alternate)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
sent = []
for tick in range(24):
now = 1_000_000_000 + tick * 40_000_000
cs, _ = feed_removed(ci, packer, now, counter=tick % 4, active=False)
assert cs.canValid and not cs.canTimeout
cc = structs.CarControl(enabled=False, latActive=False, longActive=False)
_, frames = ci.apply(cc.as_reader(), now + 2)
assert all(frame[2] == 0 for frame in frames)
sent.extend(frames)
assert any(frame[0] == 0x180 for frame in sent)
@pytest.mark.parametrize('word', [0xC150, 0xC151])
def test_removed_camera_native_has_no_forwarding_or_camera_transmit(word):
safety = libsafety_py.libsafety
release = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, word) == 0
safety.init_tests()
if release and word == 0xC151:
assert not safety.safety_config_valid()
return
for bus in (0, 1, 2):
for address in range(0x800):
assert safety.safety_fwd_hook(bus, address) == -1
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CAMERA][Bus.pt])
frame = packer.make_can_msg('PSCMStatus', 2, {})
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))
@pytest.mark.parametrize('alpha', [False, True])
def test_connected_camera_retains_its_status_delivery(alpha):
cp = camera_params(alpha=alpha)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
sent = []
for tick in range(12):
now = 1_000_000_000 + tick * 40_000_000
cs, _ = feed_camera(ci, packer, now, counter=tick % 4, active=False)
assert cs.canValid and not cs.canTimeout
_, frames = ci.apply(structs.CarControl().as_reader(), now + 2)
sent.extend(frames)
assert any(frame[0] == 0x184 and frame[2] == 2 for frame in sent)
@@ -1,65 +0,0 @@
"""A missing-camera ACC fault must not masquerade as engaged cruise."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm import gmcan
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_removed import feed_removed, removed_params
from opendbc.car.gm.values import DBC
from opendbc.safety.tests.libsafety import libsafety_py
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('alternate', [False, True])
@pytest.mark.parametrize('state', [0, 1, 2, 3, 4])
def test_removed_camera_cruise_state_keeps_real_fault(alpha, alternate, state):
cp = removed_params(alpha=alpha, alternate=alternate)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for tick in range(20):
now = 1_000_000_000 + tick * 40_000_000
feed_removed(ci, packer, now, counter=tick % 4)
status = packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': state})
out = ci.update([(now + 2, [status])])
assert out.canValid
assert out.accFaulted == (state == 3)
assert out.cruiseState.enabled == (state in (1, 2, 4))
assert out.cruiseState.standstill == (state == 4)
@pytest.mark.parametrize('active', [1, 2, 4])
def test_stock_removed_camera_fault_withdraws_native_cruise_and_cancel(active):
cp = removed_params(alpha=False)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
safety = libsafety_py.libsafety
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC150) == 0
safety.init_tests()
safety.set_timer(1_000_000)
def rx(name, values):
address, data, bus = packer.make_can_msg(name, 0, values)
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
def cancel(counter):
address, data, bus = gmcan.create_buttons(packer, 0, counter, 6)
return safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
rx('ECMEngineStatus', {'CruiseMainOn': 1})
rx('AcceleratorPedal2', {'CruiseState': active})
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 0,
'SteeringButtonChecksum': 0xFF})
assert safety.get_controls_allowed()
assert cancel(0)
rx('AcceleratorPedal2', {'CruiseState': 3})
assert not safety.get_controls_allowed()
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 1,
'SteeringButtonChecksum': 0xFF + 0x4EF})
assert not cancel(1)
# A subsequent physical active state can recover; a fault is not a latch exemption.
rx('AcceleratorPedal2', {'CruiseState': active})
assert safety.get_controls_allowed()
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 2,
'SteeringButtonChecksum': 0xFF + 2 * 0x4EF})
safety.set_timer(1_040_001)
assert cancel(2)
@@ -1,130 +0,0 @@
"""No-camera/no-ACC Volt: independent F1 brake source, conventional cruise only."""
import pytest
from types import SimpleNamespace
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_automatic import discover, recorded_fingerprint
from opendbc.car.gm.tests.test_bolt_cc import feed
from opendbc.car.gm.values import CAR, DBC, is_volt_cc_profile
from opendbc.safety.tests.libsafety import libsafety_py
def params(release=False):
return CarInterface.get_params(CAR.CHEVROLET_VOLT_CC, recorded_fingerprint(), [], False, release, False)
def tick(ci, packer, now, *, counter=0, active=True, gas=False, brake=False, regen=False, analog=0, fault=True):
_, frames = feed(SimpleNamespace(update=lambda messages: None), packer, now,
counter=counter, active=active, stock=60. / 3.6, gas=gas, brake=brake, regen=regen, speed=20.)
frames = [m for m in frames if m[2] == 0 and m[0] not in (0xBE, 0x1C4, 0x1F5)]
frames += [packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': analog}),
packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 3 if fault else 0, 'AcceleratorPedal2': 30 if gas else 0}),
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 4})]
return ci.update([(now, frames)]), frames
def test_recorded_noacc_collision_uses_conventional_owner():
assert discover(recorded_fingerprint())[0] == CAR.CHEVROLET_VOLT_CC
cp = params()
assert not cp.dashcamOnly and is_volt_cc_profile(cp)
assert cp.networkLocation == structs.CarParams.NetworkLocation.gateway
assert cp.safetyConfigs[0].safetyParam == 0xC163
assert not cp.alphaLongitudinalAvailable and cp.radarUnavailable
@pytest.mark.parametrize('active', [False, True])
@pytest.mark.parametrize('fault', [False, True])
def test_actual_f1_parser_uses_ordinary_cruise_despite_unused_acc_field(active, fault):
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for i in range(30):
cs, _ = tick(ci, packer, 1_000_000_000 + i * 40_000_000, counter=i % 4, active=active, fault=fault)
assert cs.canValid and not cs.canTimeout
assert cs.cruiseState.enabled == active
assert not cs.accFaulted and not cs.cruiseState.standstill
assert cs.cruiseState.speed == pytest.approx(60. / 3.6)
assert ci.CS.volt_cc_physical.sources_current(2_160_000_000)
@pytest.mark.parametrize('brake,analog,regen', [(False, 0, False), (True, 0, False), (False, 6, False), (False, 0, True)])
def test_f1_driver_inputs_are_preserved_host(brake, analog, regen):
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for i in range(30):
now = 1_000_000_000 + i * 40_000_000
cs, frames = tick(ci, packer, now, counter=i % 4, brake=brake, analog=analog, regen=regen)
assert cs.brakePressed == (brake or analog >= 6)
assert cs.regenBraking == regen
@pytest.mark.parametrize('driver', ['none', 'foot', 'analog', 'paddle', 'gas'])
def test_native_f1_stock_permission_and_driver_withdrawal(driver):
safety = libsafety_py.libsafety
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC163) == 0
safety.init_tests()
for i in range(30):
now = 1_000_000_000 + i * 40_000_000
safety.set_timer(now // 1000)
_, frames = tick(ci, packer, now, counter=i % 4, gas=driver == 'gas', brake=driver == 'foot',
analog=6 if driver == 'analog' else 0, regen=driver == 'paddle')
for address, data, bus in frames:
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
if not debug:
assert not safety.safety_config_valid()
return
safety.safety_tick()
assert safety.safety_config_valid()
assert bool(safety.get_controls_allowed()) == (driver in ('none', 'gas'))
for address, length, bus in [(0x315, 5, 0), (0x2CB, 8, 0), (0x200, 6, 0), (0x184, 8, 2)]:
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, bytes(length)))
for bus in (0, 1, 2):
for address in (0x180, 0x184, 0x1E1, 0x315, 0x2CB):
assert safety.safety_fwd_hook(bus, address) == -1
def test_release_f1_factory_keeps_existing_volt_cc_denial():
cp = params(release=True)
assert cp.dashcamOnly and not cp.openpilotLongitudinalControl
assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput
def test_native_f1_button_slots_cannot_be_reused_or_survive_stale_sources():
safety = libsafety_py.libsafety
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC163) == 0
safety.init_tests()
for i in range(40):
now = 1_000_000_000 + i * 40_000_000
safety.set_timer(now // 1000)
_, frames = tick(ci, packer, now, counter=i % 4)
for address, data, bus in frames:
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
ci.CC.frame = 156
command = structs.CarControl(enabled=True, latActive=True, longActive=True)
command.hudControl.setSpeed = 25.
command.actuators.accel = 1.
_, frames = ci.apply(command.as_reader(), now + 1)
assert all(bus == 0 for _, _, bus in frames)
assert not any(address in (0x315, 0x2CB, 0x2CD, 0x200, 0x370) for address, _, _ in frames)
buttons = [m for m in frames if m[0] == 0x1E1]
assert len(buttons) == 1
address, data, bus = buttons[0]
packet = libsafety_py.make_CANPacket(address, bus, data)
assert bool(safety.safety_tx_hook(packet)) == debug
assert not safety.safety_tx_hook(packet)
safety.set_timer(now // 1000 + 300_001)
assert not safety.safety_tx_hook(packet)
safety.set_timer(now // 1000 + 2_000_000)
safety.safety_tick()
assert not safety.safety_config_valid()
@@ -69,7 +69,7 @@ class TestVoltSdgmControl(unittest.TestCase):
setup(cp)
for tick in range(16):
now = 1_000_000_000 + tick * 40_000_000
out, sources = feed_camera(ci, packer, now, counter=tick % 4, gas=tick >= 8, low=tick % 2 == 0, eps_status=1)
out, sources = feed_camera(ci, packer, now, counter=tick % 4, gas=tick >= 8, low=tick % 2 == 0)
self.assertTrue(out.canValid)
for source in sources:
native("rx", source, now // 1000)
+2 -26
View File
@@ -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:
@@ -842,24 +827,15 @@ STEER_THRESHOLD = 1.0
DBC = CAR.create_dbc_map()
VOLT_CC_F1_WORD = 0xC163
def is_volt_cc_f1_profile(cp: CarParams) -> bool:
return is_volt_cc_profile(cp) and bool(cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG)
def is_volt_cc_profile(cp: CarParams) -> bool:
"""Exact development-only conventional-cruise owner, with either startup speed choice."""
try:
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_VOLT_CC and
cp.networkLocation == CarParams.NetworkLocation.gateway and not cp.alphaLongitudinalAvailable and
not cp.pcmCruise and cp.radarUnavailable and
not cp.passive and not cp.dashcamOnly and not cp.notCar and
control_flags(cp) == int(GMFlags.CC_LONG | (GMFlags.NO_ACCELERATOR_POS_MSG if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else 0)) and
not cp.passive and not cp.dashcamOnly and not cp.notCar and control_flags(cp) == int(GMFlags.CC_LONG) and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
int(cp.safetyConfigs[0].safetyParam) ==
(VOLT_CC_F1_WORD if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)))
int(cp.safetyConfigs[0].safetyParam) == int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC))
except (AttributeError, IndexError, TypeError, ValueError):
return False
@@ -7,7 +7,7 @@ def qualified(cp):
return (cp.carFingerprint == CAR.HONDA_ACCORD_11G and cp.brand == "honda" and
not cp.passive and not cp.dashcamOnly and not cp.notCar and
cp.transmissionType in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) and
cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.alternativeExperience in (0, 32) and
cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.alternativeExperience == 0 and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hondaBosch and
cp.safetyConfigs[0].safetyParam == (80 | int(bool(cp.flags & HondaFlags.BOSCH_ALT_BRAKE))) and
not int(cp.flags) & ~(int(CAR.HONDA_ACCORD_11G.config.flags) |
@@ -20,10 +20,6 @@ class AccordMvlStock:
self.last_lkas_button_frame = 0
def update(self, packer, can, cp, control, state, frame):
# The independent session can outlive the PCM. Its prior control envelope
# must not keep camera button substitution alive after physical withdrawal.
if cp.alternativeExperience == 32 and (state.out.brakePressed or not state.out.cruiseState.enabled):
return []
if not control.enabled or frame % 4 or control.cruiseControl.cancel or control.cruiseControl.resume:
return []
if (self.lkas_button_send_remaining == 0 and state.lkas_hud["LKAS_READY"] and
@@ -36,10 +36,7 @@ def qualified(cp):
cp.openpilotLongitudinalControl and not cp.pcmCruise and
not cp.passive and not cp.dashcamOnly and not cp.notCar and len(configs) == 1 and
configs[0].safetyModel == structs.CarParams.SafetyModel.hondaNidec and
(configs[0].safetyParam == (36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32) or
(len(cp.safetyConfigs) == 1 and cp.alternativeExperience == 32 and
configs[0].safetyParam == ((36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32) | 512 |
(256 if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN else 0)))))
configs[0].safetyParam == (36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32))
def create_command(packer, gas_amount, counter):
+25 -79
View File
@@ -1,5 +1,5 @@
from opendbc.car import structs
from opendbc.car.honda.values import CAR, HondaFlags, HondaSafetyFlags
from opendbc.car.honda.values import CAR, HondaFlags
CLASSIC_BOSCH = frozenset((CAR.HONDA_NBOX_2G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH,
CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID, CAR.ACURA_RDX_3G,
@@ -26,94 +26,40 @@ NIDEC = frozenset((
CAR.ACURA_MDX_3G_MMR,
CAR.ACURA_TLX_1G,
))
CANFD_STOCK = frozenset((CAR.HONDA_CRV_6G, CAR.HONDA_PILOT_4G,
CAR.HONDA_PASSPORT_4G, CAR.ACURA_MDX_4G_MMR, CAR.ACURA_TLX_2G_MMR))
ALT_RADAR_STOCK = frozenset((CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR,
CAR.ACURA_TLX_2G, CAR.ACURA_MDX_4G))
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11, 16, 17, 80, 81, 512, 513, 1058, 1059, 1570, 1571, 2082, 2083, 2211))
NIDEC_WORDS = frozenset((0, 4, 260, 544, 548, 804))
MANUAL_SELECTION_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
# qualified() runs several times per 100 Hz frame in card and selfdrived, so
# everything that depends only on this module is computed once, and its flag
# tests use plain ints: IntFlag operators cost microseconds each in Python.
_DETECTED = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM |
HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID | HondaFlags.GAS_INTERCEPTOR)
_ALLOWED_FLAGS = {str(car): int(car.config.flags) | _DETECTED
for car in CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}}
_BOSCH, _BOSCH_RADARLESS, _BOSCH_ALT_BRAKE = int(HondaFlags.BOSCH), int(HondaFlags.BOSCH_RADARLESS), int(HondaFlags.BOSCH_ALT_BRAKE)
_NIDEC, _NIDEC_ALT_SCM_MESSAGES = int(HondaFlags.NIDEC), int(HondaFlags.NIDEC_ALT_SCM_MESSAGES)
_UNSUPPORTED = int(HondaFlags.BOSCH_ALT_RADAR)
_FIXED = structs.CarParams.FingerprintSource.fixed
_TRANSMISSIONS = (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt)
_HONDA_NIDEC, _HONDA_BOSCH = structs.CarParams.SafetyModel.hondaNidec, structs.CarParams.SafetyModel.hondaBosch
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11))
NIDEC_WORDS = frozenset((0, 4, 260))
def qualified(cp, *, marked_only=False):
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) not in (1, 2) or
(len(cp.safetyConfigs) == 2 and not (cp.carFingerprint in CANFD_STOCK and
cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput and cp.safetyConfigs[0].safetyParam == 0)) or
(cp.fingerprintSource == _FIXED and cp.carFingerprint not in MANUAL_SELECTION_PORTS) or cp.transmissionType not in _TRANSMISSIONS or
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) != 1 or
cp.fingerprintSource == structs.CarParams.FingerprintSource.fixed or
cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) or
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
return False
safety = cp.safetyConfigs[-1]
fingerprint = str(cp.carFingerprint)
allowed = _ALLOWED_FLAGS.get(fingerprint)
if allowed is None:
safety = cp.safetyConfigs[0]
identity = next((candidate for candidate in CLASSIC_BOSCH | NIDEC | RADARLESS if candidate == cp.carFingerprint), None)
if identity is None:
return False
flags = int(cp.flags)
if flags & ~allowed:
detected = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM | HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID)
if int(cp.flags) & ~(int(identity.config.flags) | detected):
return False
if flags & int(HondaFlags.GAS_INTERCEPTOR) and fingerprint not in NIDEC:
flags = HondaFlags(cp.flags)
if flags & (HondaFlags.BOSCH_CANFD | HondaFlags.BOSCH_ALT_RADAR):
return False
if fingerprint in ALT_RADAR_STOCK:
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
if cp.openpilotLongitudinalControl:
expected |= int(HondaSafetyFlags.BOSCH_LONG)
if cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.ALT_RADAR_AOL | HondaSafetyFlags.AOL_BOSCH_LONG)
if fingerprint == CAR.ACURA_MDX_4G and cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.STOCK_TJA)
return (bool(flags & _BOSCH) and bool(flags & int(HondaFlags.BOSCH_ALT_RADAR)) and
not bool(flags & int(HondaFlags.BOSCH_CANFD)) and bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if flags & _UNSUPPORTED:
return False
if fingerprint == CAR.HONDA_ACCORD_11G:
from opendbc.car.honda.accord_mvl_stock import qualified as qualified_mvl
return qualified_mvl(cp)
if fingerprint in CANFD_STOCK:
expected = 16 | int(bool(flags & _BOSCH_ALT_BRAKE))
return (bool(flags & _BOSCH) and bool(flags & int(HondaFlags.BOSCH_CANFD)) and
cp.pcmCruise and not cp.openpilotLongitudinalControl and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if flags & int(HondaFlags.BOSCH_CANFD):
return False
if fingerprint in NIDEC and flags & _NIDEC and not flags & _BOSCH:
expected = 4 if flags & _NIDEC_ALT_SCM_MESSAGES else 0
pedal = bool(flags & int(HondaFlags.GAS_INTERCEPTOR))
if pedal:
expected |= int(HondaSafetyFlags.GAS_INTERCEPTOR)
if cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.NIDEC_PEDAL_AOL)
if fingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
if cp.carFingerprint in NIDEC and flags & HondaFlags.NIDEC and not flags & HondaFlags.BOSCH:
expected = 4 if flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 0
if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
expected |= 256
return (bool(cp.pcmCruise) == (not pedal) and cp.openpilotLongitudinalControl and
safety.safetyModel == _HONDA_NIDEC and safety.safetyParam == expected)
if fingerprint in CLASSIC_BOSCH and flags & _BOSCH and not flags & _BOSCH_RADARLESS:
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
if cp.openpilotLongitudinalControl:
expected |= int(HondaSafetyFlags.BOSCH_LONG)
if fingerprint == CAR.ACURA_RDX_3G:
expected |= int(HondaSafetyFlags.RDX_HIGH_GAS)
if cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.CLASSIC_LONG_AOL | HondaSafetyFlags.AOL_BOSCH_LONG)
return (cp.pcmCruise and cp.openpilotLongitudinalControl and
safety.safetyModel == structs.CarParams.SafetyModel.hondaNidec and safety.safetyParam == expected)
if cp.carFingerprint in CLASSIC_BOSCH and flags & HondaFlags.BOSCH and not flags & HondaFlags.BOSCH_RADARLESS:
expected = int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE))
return (cp.pcmCruise and not cp.openpilotLongitudinalControl and
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
if cp.carFingerprint in RADARLESS and flags & HondaFlags.BOSCH_RADARLESS:
expected = 8 | int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if fingerprint in RADARLESS and flags & _BOSCH_RADARLESS:
expected = 8 | int(bool(flags & _BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
return False
@@ -26,7 +26,7 @@ def test_factory_keeps_stock_acc_without_long_handover(alpha, release):
assert not qualified(cp)
cp = final_cp()
cp.alternativeExperience = 32
assert qualified(cp)
assert not qualified(cp)
def test_real_parser_controller_camera_button_cadence_and_override_exclusion():
@@ -1,93 +0,0 @@
"""Exact DEBUG alpha owner retains physical LAT intent without restoring LONG."""
import os
from unittest.mock import patch
import pytest
from opendbc.car import structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.radar_interface import RadarInterface
from opendbc.car.honda.tests.test_alt_radar_stock_aol_parity import IDENTITIES, factory
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
def test_actual_alpha_factory_card_serialization_and_explicit_off(identity, alt):
from opendbc.car.honda.stock_aol import qualified
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.car.honda.aol import native_accepts_cp, policy_for
initial = factory(identity, alt=alt, alpha=True)
assert initial.openpilotLongitudinalControl and not initial.pcmCruise
assert qualified(initial)
addition = 1568 if str(identity) == 'ACURA_MDX_4G' else 1056
assert policy_for(initial).safety_param_addition == addition
base_word = initial.safetyConfigs[0].safetyParam
for enabled in (False, True):
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1'}):
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', enabled, block=True)
cp = initial.as_reader().as_builder()
card = Car(CI=CarInterface(cp), RI=RadarInterface(cp))
assert card.aol_qualified == enabled
assert card.CP.safetyConfigs[0].safetyParam == base_word | (addition if enabled else 0)
assert card.CP.alternativeExperience == (32 if enabled else 0)
assert card.CP.openpilotLongitudinalControl and not card.CP.pcmCruise
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
assert published.to_dict() == card.CP.to_dict()
assert native_accepts_cp(card.CP, 20, card.CP.safetyConfigs[0].safetyParam) == enabled
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
def test_actual_native_set_brake_and_host_axes_do_not_auto_resume_long(identity, alt):
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case, car_state
with OpenpilotPrefix():
case = honda_family_case('honda', True)
case.family_cp = factory(identity, alt=alt, alpha=True).as_reader()
native = TestHondaStockAol(methodName='runTest')
try:
sd, _, drive, card, receive = case.family_callers(True)
word = card.CP.safetyConfigs[0].safetyParam
assert word in (1058, 1059, 1570, 1571)
native.configure(('alt-alpha', 20, word, 1, card.CI.CC.packer.dbc.name, False, 'GEARBOX_AUTO', 4))
native.stream()
native.normal_engage() # Actual SET falling edge from checksummed CAN.
native.safety.aol_set_host_request(3)
if not native.debug:
assert native.safety.aol_get_permission_mask() == 0
return # RELEASE native refusal is authoritative, never fabricated grant.
assert native.safety.aol_get_permission_mask() == 3
axis, _, command = drive(2, allowed=True, native_request_long=True,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
native.press_brake()
assert not native.safety.get_controls_allowed()
native.safety.aol_set_host_request(1)
permission = native.safety.aol_get_permission_mask()
assert permission == 1
state = car_state(brake=True)
axis, _, command = drive(3, observed_state=state, allowed=bool(permission & 1), native_request_long=False,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(10_030_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=state)
native.stream()
axis, _, command = drive(4, native_request_long=False, allowed=bool(native.safety.aol_get_permission_mask() & 1),
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert not native.safety.get_controls_allowed()
assert axis.lateralActive and command.latActive and not axis.longitudinalActive and not command.longActive
native.stream(gear=2)
assert native.safety.aol_get_permission_mask() == 0
native.stream()
assert native.safety.aol_get_permission_mask() == 1
native.stream(eps=7)
native.stream(eps=0)
assert native.safety.aol_get_permission_mask() == 0
finally:
libsafety_py.libsafety.set_alternative_experience(0)
case.doCleanups()
@@ -1,109 +0,0 @@
"""ALT_RADAR stock AOL keeps actual factory ownership and exact MDX TJA semantics."""
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR, HondaSafetyFlags
IDENTITIES = (CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR, CAR.ACURA_TLX_2G, CAR.ACURA_MDX_4G)
def factory(identity, *, alt=False, release=False, alpha=False):
fp = gen_empty_fingerprint()
fp[1][0x1A3] = 8
if alt:
fp[1][0x1BE] = 3
return CarInterface.get_params(identity, fp, [], alpha, release, False)
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
@pytest.mark.parametrize('release', (False, True))
def test_alt_radar_stock_factory_and_native_marking(identity, alt, release):
from opendbc.car.honda.stock_aol import qualified
from openpilot.starpilot.car.honda.aol import native_accepts_cp, policy_for
cp = factory(identity, alt=alt, release=release)
if identity == CAR.ACURA_RDX_3G_MMR and release:
assert cp.dashcamOnly and not qualified(cp)
return
assert qualified(cp)
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
policy = policy_for(cp)
assert policy.explicit_latch and policy.normal_runtime_supported
expected = int(HondaSafetyFlags.STOCK_TJA) if identity == CAR.ACURA_MDX_4G else 0
assert policy.safety_param_addition == expected
base = cp.safetyConfigs[0].safetyParam
cp.safetyConfigs[0].safetyParam |= expected
cp.alternativeExperience = 32
assert qualified(cp, marked_only=True)
assert cp.safetyConfigs[0].safetyParam == base | expected
assert native_accepts_cp(cp, 20, base | expected)
bad = cp.as_reader().as_builder()
bad.safetyConfigs[0].safetyParam ^= 512
assert not qualified(bad)
assert qualified(factory(identity, alt=alt, alpha=True))
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
def test_actual_card_controls_and_native_sources_use_final_stock_cp(identity, alt):
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
cp = factory(identity, alt=alt)
with OpenpilotPrefix():
case = honda_family_case('honda', False)
case.family_cp = cp.as_reader()
native = TestHondaStockAol(methodName='runTest')
try:
sd, _, drive, card, receive = case.family_callers(False)
assert cp.pcmCruise and not card.CP.openpilotLongitudinalControl
word = card.CP.safetyConfigs[0].safetyParam
native.configure(('alt-radar', 20, word, 1, card.CI.CC.packer.dbc.name, False, 'GEARBOX_AUTO', 4))
native.stream()
native.normal_engage()
native.press_brake()
assert not native.safety.get_controls_allowed()
permission = native.safety.aol_get_permission_mask()
assert permission == 1
axis, _, command = drive(2, allowed=bool(permission & 1), native_request_long=False,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
native.stream(eps=1)
permission = native.safety.aol_get_permission_mask()
assert permission == int(identity == CAR.ACURA_MDX_4G)
if identity == CAR.ACURA_MDX_4G:
# The configured current parser explicitly regards this status as healthy.
from opendbc.can import CANPacker
packer = CANPacker(card.CI.CC.packer.dbc.name)
msg = packer.make_can_msg('STEER_STATUS', 1, {'STEER_STATUS': 1})
card.CI.update([])
parsed = card.CI.update([(10_030_000_000, [msg])])
assert not parsed.steerFaultPermanent and not parsed.steerFaultTemporary
axis, _, command = drive(3, allowed=bool(permission & 1), native_request_long=False,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert bool(axis.lateralActive) == bool(permission & 1)
assert not command.longActive
native.stream(eps=7)
native.stream(eps=0)
assert native.safety.aol_get_permission_mask() == 0
finally:
libsafety_py.libsafety.set_alternative_experience(0)
case.doCleanups()
@pytest.mark.parametrize('word', (512, 513))
def test_tja_marker_cannot_be_reused_by_other_host_identity(word):
from opendbc.car.honda.stock_aol import qualified
for identity in (CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR, CAR.ACURA_TLX_2G):
cp = factory(identity)
cp.alternativeExperience = 32
cp.safetyConfigs[0].safetyParam = word
assert not qualified(cp)
cp = factory(CAR.ACURA_MDX_4G)
cp.alternativeExperience = 32
cp.safetyConfigs = [structs.CarParams.SafetyConfig(safetyModel='noOutput'), cp.safetyConfigs[0]]
assert not qualified(cp)

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