mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-01 03:24:05 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b1ce61fb8 | |||
| 6c93a41942 | |||
| b4154fdc5b | |||
| ea7de662dc | |||
| cc5b618884 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,90 +1,16 @@
|
||||
# StarPilot
|
||||
|
||||
StarPilot is an independent fork of [openpilot](https://github.com/commaai/openpilot), with custom driving controls, vehicle integrations, raylib interfaces for comma 3 and comma four, and the Galaxy companion interface.
|
||||
|
||||
[](https://deepwiki.com/StarPilotHQ/StarPilot)
|
||||
[](https://firestar.link/discord)
|
||||
[](https://github.com/StarPilotHQ/StarPilot)
|
||||
[](https://wiki.firestar.link)
|
||||
[](https://apps.apple.com/ca/app/starpilot-bt/id6821039119)
|
||||
[](https://groups.google.com/g/galaxy-starpilot-android-beta)
|
||||
**Domathon is the development branch for StarPilot 7.0.** It rebuilds StarPilot on a current openpilot foundation. Feature migration and vehicle qualification are still in progress; this branch is not a completed replacement for the existing Dom release.
|
||||
|
||||
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
|
||||
an open source driver assistance system.
|
||||
## Explore the project
|
||||
|
||||
**This is the StarPilot 7.0 development branch (`SecretGoodStarPilot`). Feature migration,
|
||||
vehicle qualification, and Auto Learn validation are still in progress.**
|
||||
|
||||
Openpilot provides
|
||||
* Automated Lane Centering
|
||||
* Adaptive Cruise Control
|
||||
* Lane Change Assist
|
||||
* Driver Monitoring *without wheel nags*
|
||||
|
||||
Ford-specific lateral-control and vehicle-support work includes substantial adaptations from
|
||||
[BluePilot](https://github.com/BluePilotDev/bluepilot/tree/bp-7.0), principally developed by
|
||||
[Alan Polk](https://github.com/alan-polk) with additional BluePilot contributors. StarPilot's
|
||||
implementation has since diverged, but that does not erase its lineage. See [CREDITS.md](CREDITS.md)
|
||||
for the code-level provenance and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for applicable
|
||||
upstream notices and terms. BluePilot and its contributors do not maintain or endorse StarPilot;
|
||||
please direct support requests for this adaptation to the StarPilot project.
|
||||
|
||||
Hyundai, Kia, and Genesis angle steering and related vehicle support include substantial adaptations
|
||||
from [sunnypilot](https://github.com/sunnypilot/sunnypilot/tree/hkg-angle-steering-2025) and its
|
||||
[opendbc angle-steering branch](https://github.com/sunnypilot/opendbc/tree/hkg-angle-steering-2025).
|
||||
StarPilot's implementation has diverged significantly; the upstream contributors do not maintain
|
||||
this adaptation. Detailed code lineage is recorded in [CREDITS.md](CREDITS.md).
|
||||
|
||||
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
|
||||
Stop by to chat or ask questions!
|
||||
If your car isn't driving as well as you'd like, [open a ticket so we can help](https://firestar.link/logs)!
|
||||
|
||||
## Documentation
|
||||
|
||||
Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists,
|
||||
installation guides, and software configuration.
|
||||
|
||||
## Features
|
||||
|
||||
* **Full support for Comma C3, C3X, and C4**
|
||||
* Full support for chestnut/eGPU on C3, C3X, and C4. C3/C3X may require special gps handling.
|
||||
* Model switcher with all of comma's tinygrad driving models
|
||||
* StarPilot custom torque controller
|
||||
* StarPilot custom longitudinal planner for both radar and VoACC (visual only, radar-less) vehicles
|
||||
* **Custom tuning for an expanding list of vehicles**
|
||||
* Firestar Lateral Method -
|
||||
Auto tuning system to adapt openpilot to *your* car's unique lateral characteristics.
|
||||
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
|
||||
Download models, change settings, update software, diagnostics, manage dashcam video, and more.
|
||||
* Navigation - Display routes and give the driving model hints on taking exits and turns.
|
||||
* Always On Lateral (full time steering assist)
|
||||
* Lane Centering - Enable to center between detected lane lines,
|
||||
with graceful fallback to model E2E lateral outputs and safety breakout
|
||||
* Force Stop 2.0 - Better stops at stop signs and traffic lights.
|
||||
* Speed Limit Controller - Use dashboard, map data, and/or vision detection for speed limit signs
|
||||
* Curve Speed Controller - Learns from how you take curves
|
||||
* Conditional Experimental Mode (CEM)
|
||||
* Conditional Chill Mode (CCM)
|
||||
* Driving Profiles and Traffic Mode
|
||||
* Custom themes
|
||||
* Alert volume customization
|
||||
* Comma Pedal Interceptor support
|
||||
* Bluetooth support - Connect keyboards, controllers, steering wheel remotes, and more.
|
||||
* Sentry mode - Record when motion is detected while parked.
|
||||
* V-ASM - Detect vehicles in the adjacent/blind spots using the driver-facing camera.
|
||||
* Additional car support - Toyota SDSU, ZSS, CAN filter; pre-AP tesla; ford; Polestar 2; and more.
|
||||
* And more!
|
||||
|
||||
|
||||
## GM-only Features
|
||||
|
||||
* Increased LKAS fault resiliency
|
||||
* ASCM_INT and SASCM support
|
||||
* Custom lateral torque controller, with special tuning for Bolts
|
||||
* 50% extra torque on 2017 Chevy Bolt
|
||||
* Improved lateral and longitudinal tuning
|
||||
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
|
||||
* Extra steering wheel button functionality for vehicles with pedal interceptor
|
||||
* Optional toggle to boot comma when remote starting your vehicle
|
||||
- [Developer commands](tools/STARPILOT_DEVELOPMENT.md): host tools, replay, and device builds.
|
||||
- [Driving interfaces](openpilot/starpilot/ui/README.md) and [Galaxy](openpilot/starpilot/galaxy/README.md): native and companion controls.
|
||||
- [Driving models](openpilot/starpilot/models/README.md): model management, artifact compatibility, and Model Laboratory status.
|
||||
- [Builds and developer tools](docs/how-to/laptop-device-build.md): local development and device builds.
|
||||
- [Dependency maintenance](tools/vendor/README.md): tracked source folders and upstream sync records.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -110,10 +36,8 @@ StarPilot retains upstream vehicle and Panda safety interfaces and adds regressi
|
||||
|
||||
Support depends on the vehicle configuration and enabled feature. An existing vehicle identifier or settings page does not by itself establish driving support. Model Laboratory currently exposes management controls, but paired driving inference remains unavailable pending integration and Chestnut validation.
|
||||
|
||||
## Third-Party Notices
|
||||
## Upstream and attribution
|
||||
|
||||
* Portions of this software include modified versions of the Material Design Icons provided by Google under the Apache License 2.0. A copy of the license is included in the `LICENSE-MDI` file.
|
||||
* Ford support includes software adapted from BluePilot's `bp-7.0` branch. The source repository contains both a standard MIT notice and a separate custom SUNNYPILOT LLC notice; StarPilot preserves both in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
* Hyundai, Kia, and Genesis support includes software adapted directly from sunnypilot's HKG angle-steering branch and sunnypilot/opendbc. StarPilot preserves the applicable notices in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
The current openpilot baseline is [`78dccf0b482ea1298fd1f6fa59b0554187b02ce6`](https://github.com/commaai/openpilot/commit/78dccf0b482ea1298fd1f6fa59b0554187b02ce6). Its history remains intact, followed by the StarPilot foundation and feature commits. Upstream development continues independently; the baseline changes only after an update has been integrated and checked.
|
||||
|
||||
> This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
|
||||
See [LICENSE](LICENSE), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and [CREDITS.md](CREDITS.md) for licensing and source attribution. Component licenses remain with their source folders. StarPilot is maintained independently of comma.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_BIN="${HEATDEATH_PYTHON:-python3}"
|
||||
if [[ -z "${HEATDEATH_PYTHON:-}" && -x /usr/local/venv/bin/python3 ]]; then
|
||||
PYTHON_BIN=/usr/local/venv/bin/python3
|
||||
fi
|
||||
exec "$PYTHON_BIN" -B "$ROOT_DIR/tools/heatdeath/heatdeath.py" "$@"
|
||||
@@ -10,4 +10,4 @@ jetlink: run openpilot's large driving models on an attached Jetson.
|
||||
# so does the release check (macos/scripts/check-version.sh). The comma runs
|
||||
# jetlink from a checkout, not an install, so this is also all it has. The Swift
|
||||
# reads it from Pinned.swift: run JetlinkKit/Scripts/make_pins.py after a bump.
|
||||
__version__ = '0.8.9'
|
||||
__version__ = '0.8.7'
|
||||
|
||||
@@ -152,16 +152,6 @@ class ModelSide(WorkerSide, Protocol):
|
||||
frame thread at a hand-back, so it zeroes the history in place and
|
||||
allocates and compiles nothing; warp.reset_queues makes one."""
|
||||
|
||||
def model_warp(self, small: Any, cam_w: int, cam_h: int, model_w: int, model_h: int) -> FrameWarp | None:
|
||||
"""Optional (OPTIONAL): the warp the link sends frames through, warmed,
|
||||
or None for openpilot's driving warp pickle at warp_path (warp.Warp). For
|
||||
a fork whose runtime cannot load that pickle, typically with its small
|
||||
model's own warp. Asked once on modeld's main thread before the frame
|
||||
loop, after the small model's warmup; raising keeps the drive on the
|
||||
small model, as a pickle that does not load does. The offroad checks
|
||||
still look for the pickle (Warps.built): only modeld has a small model
|
||||
to ask, so warp_path must name a file either way."""
|
||||
|
||||
|
||||
class Openpilot(ModelSide, Protocol):
|
||||
"""The whole adapter: one object implements every side."""
|
||||
@@ -208,28 +198,13 @@ class JoinedModel(Protocol):
|
||||
"""Let go of the link at modeld's exit."""
|
||||
|
||||
|
||||
class FrameWarp(Protocol):
|
||||
"""What an adapter's model_warp returns, warp.Warp's shape, run on modeld's
|
||||
frame thread. Only one of the small and the large model runs a frame, so a
|
||||
warp that shares the small model's JIT and buffers is never in use by both.
|
||||
On a comma, a warp that reads its GPU output back moves it first with
|
||||
warp.make_coherent, before the warp's JIT first runs."""
|
||||
output: Any # the warped frame, (2, 6, model_h/2, model_w/2) uint8 as bytes: read after wait(), until the next start()
|
||||
|
||||
def start(self, frame: int, big_frame: int, tfm: Any, big_tfm: Any) -> None:
|
||||
"""Warp the camera buffers at these addresses under their 3x3 transforms."""
|
||||
|
||||
def wait(self) -> None:
|
||||
"""Return once `output` holds what start() warped."""
|
||||
|
||||
|
||||
# The members of each protocol an implementation may lack: jetlink reads them
|
||||
# with getattr and a fallback. conformance() passes an object without one and
|
||||
# holds one with it to its signature, and adding one keeps API. Kept out of
|
||||
# the protocols, where a name would be a member itself; a type checker still
|
||||
# asks for these, as typing has no optional member
|
||||
OPTIONAL: dict[type, tuple[str, ...]] = {
|
||||
ModelSide: ('small_reset', 'model_warp'),
|
||||
ModelSide: ('small_reset',),
|
||||
SmallModel: ('desire_key', 'numpy_inputs', 'lat_delay', 'PLANPLUS_CONTROL'),
|
||||
JoinedModel: ('get_action_from_model', 'constants', 'LAT_SMOOTH_SECONDS', 'LONG_SMOOTH_SECONDS', 'lat_delay'),
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ class JoiningModelState:
|
||||
self._demote(BEHIND)
|
||||
elif self._active is not self._small and self._frame_drop_ratio > DROP_LIMIT:
|
||||
# modeld is behind the large model; this frame is the small model's
|
||||
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, " +
|
||||
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, "
|
||||
"the small model drives from this one", self._frame_drop_ratio * 100)
|
||||
self._demote(BEHIND)
|
||||
self._maybe_swap()
|
||||
@@ -745,7 +745,6 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
|
||||
link = links.Link(parts.log)
|
||||
|
||||
def prepare():
|
||||
from jetlink.openpilot.interface import FrameWarp, conformance
|
||||
from jetlink.openpilot.warp import Warp, prepare_reset
|
||||
# the gadget first, so the Jetson enumerates while the warp loads. Left to
|
||||
# the join thread the bind lands ~3 s later, behind the small model's
|
||||
@@ -760,12 +759,7 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
|
||||
try:
|
||||
# the fork's own reset of its small model, else jetlink's from its shape
|
||||
ready['reset_small'] = getattr(op, 'small_reset', lambda small: None)(small) or prepare_reset(small)
|
||||
# the fork's own warp, else openpilot's driving warp pickle
|
||||
warp = getattr(op, 'model_warp', lambda *args: None)(small, cam_w, cam_h, *geometry)
|
||||
if warp is None:
|
||||
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
|
||||
elif problems := conformance(warp, FrameWarp):
|
||||
raise RuntimeError(f"the adapter's model_warp is not a warp: {', '.join(problems)}")
|
||||
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
|
||||
except Exception:
|
||||
link.close()
|
||||
raise
|
||||
|
||||
@@ -157,9 +157,9 @@ class JetlinkModelState:
|
||||
# model manager key off this flag
|
||||
self.chestnut = True
|
||||
|
||||
# a warp.Warp or the adapter's model_warp, loaded and warmed ahead: its
|
||||
# first call links the JIT, and this runs on modeld's frame thread. Frames
|
||||
# go out of its output as they are, over either transport
|
||||
# a warp.Warp, loaded and warmed ahead: its first call links the JIT, and
|
||||
# this runs on modeld's frame thread. Frames go out of its output as they
|
||||
# are, over either transport
|
||||
self.warp = warp
|
||||
|
||||
self.input_shapes = spec.input_shapes
|
||||
@@ -305,9 +305,9 @@ class JetlinkModelState:
|
||||
# over the cable: how frames go, how many never came back, and what the
|
||||
# kernel dropped on the way in (softnet), since the link came up
|
||||
drops = getattr(self.client.t, 'net_drops', lambda: None)()
|
||||
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, " +
|
||||
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, "
|
||||
f"{self.client.frames_lost} lost, net drops {drops}" if drops is not None else '')
|
||||
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; " +
|
||||
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; "
|
||||
"server gpu %.1f queue %.1f total %.1f ms; ffs maxima prepare %.1f read_wait %.1f handoff %.1f ms%s",
|
||||
self._frame_id, (frame.t1 - frame.t0) * 1e3, (frame.t2 - frame.t1) * 1e3,
|
||||
(frame.sent - frame.t2) * 1e3, (t4 - waiting_from) * 1e3,
|
||||
|
||||
@@ -161,7 +161,7 @@ class Warp:
|
||||
out = jit.captured.ret.uop.base.buffer
|
||||
self._device = out.device
|
||||
if self._device.startswith('QCOM'):
|
||||
make_coherent(out)
|
||||
_make_coherent(out)
|
||||
# the two buffers every frame passes, written in place by start()
|
||||
self._tensors = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize()
|
||||
for name, (shape, dtype, device) in specs.items()}
|
||||
@@ -191,15 +191,10 @@ class Warp:
|
||||
self._replay(self._inputs, {})
|
||||
|
||||
|
||||
def make_coherent(out) -> None:
|
||||
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back, so
|
||||
the CPU reads it through its cache once the GPU is done (see Warp). Raises
|
||||
if the kernel keeps either flag back (the allocation's record holds its
|
||||
answer): write-back memory that is not coherent reads stale frames.
|
||||
|
||||
For an adapter's model_warp too. Move a buffer before the first call of
|
||||
the JIT that writes it: that call links its address into the JIT, which
|
||||
would keep writing the memory this frees."""
|
||||
def _make_coherent(out) -> None:
|
||||
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back.
|
||||
Raises if the kernel keeps either flag back (the allocation's record holds
|
||||
its answer): write-back memory that is not coherent reads stale frames."""
|
||||
from tinygrad.device import Device
|
||||
dev = Device[out.device]
|
||||
mem = dev._gpu_alloc(out.nbytes, flags=COHERENT_WRITEBACK)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2862
-2863
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
+4
-12
@@ -160,17 +160,13 @@ class CarController(CarControllerBase):
|
||||
else:
|
||||
self.classic_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
|
||||
previous = self.classic_lateral.curvature_last
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced or self.generic_canfd else FordLateralResult()
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced else FordLateralResult()
|
||||
bound = generic_canfd_bounded_command if self.generic_canfd else classic_bounded_command
|
||||
lateral = bound(self.classic_lateral, demanded, previous, CS.out.vEgoRaw,
|
||||
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
|
||||
self.classic_lateral_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
if self.generic_canfd:
|
||||
if self.classic_extended_announced != bool(lateral.active):
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN) if lateral.active else
|
||||
fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = bool(lateral.active)
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(create_extended_canfd_msg(
|
||||
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
|
||||
@@ -225,8 +221,7 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral_active, 0., 0., -self.apply_curvature_last, 0.))
|
||||
|
||||
# send lka msg at 33Hz
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0 and not any(
|
||||
address == 0x3CA for address, _, _ in can_sends):
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0:
|
||||
if self.CP.flags & FordFlags.LKA_STEERING:
|
||||
source_age_ns = now_nanos - CS.lkas_available_ts_nanos
|
||||
lka_active = (CC.latActive and CS.lkas_available and 0 <= source_age_ns <= 100_000_000 and
|
||||
@@ -255,11 +250,8 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.mache_extended_announced = True
|
||||
elif self.classic_lateral is not None:
|
||||
if self.generic_canfd and not self.classic_extended_announced:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
else:
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
else:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
|
||||
|
||||
@@ -57,27 +57,6 @@ class TestAngleCore(unittest.TestCase):
|
||||
self.assertTrue(resumed.active)
|
||||
self.assertLess(abs(resumed.path_angle), .02)
|
||||
|
||||
def test_retained_aol_keeps_active_after_release_and_tracking_error(self):
|
||||
for stalled in (False, True):
|
||||
core = AngleController(recovery_blips=False)
|
||||
for tick in range(100):
|
||||
inputs = Inputs(True, 15., 0., .006 if stalled else .001,
|
||||
steering_pressed=not stalled and tick < 20)
|
||||
result = core.step(inputs)
|
||||
self.assertTrue(result.active, (stalled, tick))
|
||||
self.assertLessEqual(abs(result.path_angle / (15. * limits.GAIN_MIN)), limits.LATERAL_ACCEL / 15. ** 2)
|
||||
self.assertEqual(core.blip_frames, 0)
|
||||
|
||||
def test_retained_aol_preserves_human_turn_and_invalid_input_neutral(self):
|
||||
core = AngleController(recovery_blips=False)
|
||||
for _ in range(61):
|
||||
result = core.step(Inputs(True, 15., 0., .001, steering_pressed=True, steering_angle=60.))
|
||||
self.assertEqual(result, Command())
|
||||
self.assertEqual(core.step(Inputs(True, 15., 0., math.nan)), Command())
|
||||
self.assertEqual(core.step(Inputs(False, 15., 0., .001)), Command())
|
||||
# Infeasible measured-curvature and actuator envelopes must still go neutral.
|
||||
self.assertEqual(core.step(Inputs(True, 15., .1, .001)), Command())
|
||||
|
||||
def test_pinned_bp7_pure_angle_and_actual_control_bytes_with_documented_limits(self):
|
||||
builders = reference.builders()
|
||||
bus = SimpleNamespace(main=0)
|
||||
@@ -192,7 +171,7 @@ class TestAngleTrim(unittest.TestCase):
|
||||
for changes in ({"active": False}, {"lane_change": True}, {"steering_pressed": True}):
|
||||
trim = LaneCenterTrim()
|
||||
trim.update(0., 15., original, config, active=True, lane_change=False)
|
||||
flags = {"active": True, "lane_change": False, "steering_pressed": False}
|
||||
flags = dict(active=True, lane_change=False, steering_pressed=False)
|
||||
flags.update(changes)
|
||||
self.assertEqual(trim.update(.001, 15., original, config, **flags), .001)
|
||||
self.assertEqual(trim.correction, 0.)
|
||||
@@ -232,19 +211,3 @@ class TestAngleTrim(unittest.TestCase):
|
||||
else:
|
||||
previous = 0.
|
||||
self.assertEqual(core.trim.correction, 0.)
|
||||
|
||||
|
||||
class TestAngleInputParity(unittest.TestCase):
|
||||
def test_missing_model_fallback_matches_pinned_bp7(self):
|
||||
for identity in sorted(CLASSIC | CANFD):
|
||||
gains = (.95, .95) if identity in TRUCKS else ((1., 1.05) if identity in CANFD else (1., 1.15))
|
||||
for speed in (5., 8.):
|
||||
for direction in (-1, 1):
|
||||
adapted = AngleController(low_gain=gains[0], high_gain=gains[1])
|
||||
upstream = reference.Angle(identity)
|
||||
for _ in range(20):
|
||||
inputs = Inputs(True, speed, direction * .001, direction * .001)
|
||||
command, expected = adapted.step(inputs), upstream.step(inputs)
|
||||
self.assertTrue(command.active)
|
||||
self.assertAlmostEqual(command.path_angle, expected.path_angle, places=12)
|
||||
self.assertEqual(command.precision, expected.precision_type)
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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)],
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user