Compare commits

...

124 Commits

Author SHA1 Message Date
natehuby76 0defb30c6a announce galaxy on the local network 2026-10-11 13:24:05 -04:00
firestarsdog bcafa532a2 Merge Nate's Galaxy Bluetooth companion changes into Dom 2026-10-09 23:07:06 -04:00
natehuby76 667fb666b9 clean up companion app files 2026-10-09 13:06:15 -04:00
natehuby76 32f49f8527 auto-start bluetooth bridge and back up settings 2026-10-09 12:59:31 -04:00
natehuby76 8e521e6992 move phone pairing into bluetooth settings 2026-10-09 12:59:31 -04:00
natehuby76 f8214fe6c6 add comma 3x pairing 2026-10-09 12:59:30 -04:00
natehuby76 258cbb76ec add iphone tester guide 2026-10-09 12:59:29 -04:00
natehuby76 c7c07799d3 give chestnut its own diagnostics section 2026-10-09 12:59:29 -04:00
natehuby76 7f7dbc6a65 group diagnostics and tidy settings 2026-10-09 12:59:28 -04:00
natehuby76 2e68e2d926 add galaxy, live view and diagnostics tabs 2026-10-09 12:59:27 -04:00
natehuby76 0623c1a0f2 fix live video output on comma 2026-10-09 12:59:27 -04:00
natehuby76 69999fa6cc fix live view authentication and encoder errors 2026-10-09 12:59:18 -04:00
natehuby76 1f60231a84 add copyable live view logs 2026-10-09 12:59:17 -04:00
natehuby76 a651454e7d fix live stream file collisions 2026-10-09 12:59:17 -04:00
natehuby76 8928743749 handle empty live stream frames 2026-10-09 12:59:16 -04:00
natehuby76 148cba7260 improve live view setup errors 2026-10-09 12:59:15 -04:00
natehuby76 d9127cf936 add carplay diagnostics and lan live view 2026-10-09 12:59:14 -04:00
natehuby76 6d51c06692 add starpilot branding and app icon 2026-10-09 12:59:14 -04:00
natehuby76 3a5d376b54 use nate naming and trim comments 2026-10-09 12:59:08 -04:00
natehuby76 c7fb5d545d add qr pairing and forget phones 2026-10-09 12:59:07 -04:00
natehuby76 9d36f4cd0d start bluetooth bridge on boot 2026-10-09 12:59:07 -04:00
natehuby76 8693affef0 add comma install guide and startup service 2026-10-09 12:59:06 -04:00
natehuby76 df68b82a5e merge dom with the bluetooth app changes 2026-10-09 12:59:05 -04:00
natehuby76 3980401632 handle hotspot and wifi changes 2026-10-09 12:59:05 -04:00
natehuby76 7f48d7c53b prefer lan and keep galaxy available offline 2026-10-09 12:59:04 -04:00
natehuby76 6e83349fe2 add android bluetooth prototype 2026-10-09 12:58:57 -04:00
natehuby76 89c189e528 reduce bluetooth traffic for toggles 2026-10-09 12:58:57 -04:00
natehuby76 698496dd60 stream bluetooth responses with notifications 2026-10-09 12:58:56 -04:00
natehuby76 4d4ed2b3da speed up bluetooth settings 2026-10-09 12:58:55 -04:00
natehuby76 a49d8772f3 fix large toggles responses 2026-10-09 12:58:55 -04:00
natehuby76 639450281e fix install on agnos 2026-10-09 12:58:54 -04:00
natehuby76 dc8d448b35 add iphone app and bluetooth bridge 2026-10-09 12:58:53 -04:00
firestar5683 61fd19556f panda 2026-10-08 23:00:01 -05:00
firestar5683 956aceaf0a hiya peach 2026-10-08 22:55:06 -05:00
firestarsdog 51d09a6274 Boot logo refresh on panel open 2026-10-08 17:37:57 -04:00
firestar5683 15bbbf3cc4 Quaker Oats 2026-10-08 15:10:09 -05:00
firestar5683 ebd62915a2 Alan, My Boi 2026-10-08 14:43:21 -05:00
firestar5683 934dadbcdb Toyota Hold 2026-10-08 07:49:05 -05:00
firestar5683 e75470f2e8 Overrun 2026-10-07 23:46:25 -05:00
firestar5683 6de77049b3 Potato Ole 2026-10-06 21:41:21 -05:00
firestar5683 34ecbf9204 Cabo 2026-10-05 22:50:38 -05:00
firestar5683 d9bd49fb8a build 2026-10-05 11:54:18 -05:00
firestar5683 5e8501002d Fire Sauce 2026-10-05 11:50:06 -05:00
firestar5683 c582bd3f2d Reckoning of Bell 2026-10-04 23:03:42 -05:00
firestar5683 15c7110647 The Bell 2026-10-04 21:24:41 -05:00
firestar5683 c2109496a7 Compatibility Shim 2026-10-03 23:14:12 -05:00
firestarsdog cede5ddc9d Drawer 2026-10-03 18:09:42 -04:00
firestar5683 385428f6ea gv70 2026-10-03 16:44:36 -05:00
firestarsdog f973d151c6 Try vert more 2026-10-03 16:39:20 -04:00
firestarsdog e60595ccc3 Try vert more 2026-10-03 16:13:33 -04:00
firestarsdog 7b06ec2ae6 Polish 2026-10-03 15:30:50 -04:00
firestarsdog 12947fd616 Big Smooshed SLC Stuff (lots) 2026-10-03 15:15:06 -04:00
firestarsdog 189f2aafbb polish 2026-10-03 01:40:36 -04:00
firestarsdog 2efe6017bf polish 2026-10-02 22:12:32 -04:00
firestar5683 3293a34219 build 2026-10-02 21:04:12 -05:00
firestar5683 d2cd3fd0ed Quintessence: A Quest 2026-10-02 21:03:49 -05:00
firestarsdog 4ac874f6b4 polish 2026-10-02 20:30:33 -04:00
firestarsdog 1aefe9dc82 more font size polish 2026-10-02 20:03:46 -04:00
firestarsdog 0857acfc50 big ui text size polish 2026-10-02 14:24:05 -04:00
firestar5683 5c3ddd58e1 build 2026-10-01 21:09:41 -05:00
firestar5683 7848a0090a Soup 2026-10-01 21:08:43 -05:00
firestar5683 86599a27bc Flood Warning 2026-10-01 14:11:28 -05:00
Dom d9f58acbb7 Merge pull request #194 from N30-PH/starpilot-honda-city-2025-firmware
Honda: add observed 2025 City firmware versions
2026-10-01 12:43:20 -05:00
firestar5683 42d4a5b207 Jalisco's 2026-09-30 17:16:41 -05:00
N30 b22e2d678a Honda City: document Brazilian model years 2023-25
Follow sunnypilot/opendbc#487 and commaai/opendbc#3812.
Only update the existing documentation label; vehicle parameters are unchanged.
2026-09-30 16:13:34 -03:00
N30 26433e078e Honda: add observed 2025 City firmware versions
Add six firmware versions from the owner's recorded City EXL 2025 inventory.
Preserve existing firmware and control settings. Credit baninfelipe and
sunnypilot/opendbc#487; related upstream contribution commaai/opendbc#3812.
2026-09-30 16:13:21 -03:00
firestar5683 bf1b916d50 Aldi 2026-09-29 20:22:20 -05:00
firestar5683 faf5b53321 Ferd Long 2026-09-28 14:57:33 -05:00
firestar5683 ac8028a21e panda 2026-09-28 13:52:16 -05:00
firestar5683 96ef704da7 Desires 2026-09-28 13:51:49 -05:00
firestarsdog 8d01d881cb polygons 2026-09-28 01:25:40 -04:00
firestarsdog 7d2f012387 model renderer optimization 2026-09-28 00:44:41 -04:00
firestarsdog da7af42e49 PathEdge cleanup 2026-09-27 23:13:35 -04:00
firestarsdog b1e7a5c46e Favorite menu cleanup 2026-09-27 22:45:20 -04:00
firestar5683 7b4643208e EV9 2026-09-27 16:14:22 -05:00
firestar5683 2dd44a6368 AOL/TESLA 2026-09-27 12:44:53 -05:00
firestar5683 19b2264e43 build 2026-09-27 11:02:12 -05:00
firestar5683 a19ed91f61 Tunes and Bugfixes 2026-09-27 11:01:27 -05:00
firestarsdog 04c2353096 You & I 2026-09-27 00:59:39 -04:00
firestar5683 6cae0cebe7 Accept AGNOS 19.8.2 2026-09-25 15:52:36 -05:00
firestar5683 ba901b5f55 panda 2026-09-25 12:25:25 -05:00
firestar5683 e6a60d6cba leaky faucet 2026-09-25 12:25:05 -05:00
firestar5683 d638e62811 build 2026-09-24 22:14:06 -05:00
firestar5683 18465ed4ef ray pedal adjust 2026-09-24 22:13:45 -05:00
firestar5683 f5672221a6 Agnos Update 2026-09-24 21:45:50 -05:00
firestar5683 99c5efa680 build 2026-09-24 17:39:10 -05:00
firestar5683 1648a100e3 alt path brake hold 2026-09-24 17:38:49 -05:00
firestar5683 d3a74f61e4 build 2026-09-24 17:13:20 -05:00
firestar5683 0c6ee69362 Long day 2026-09-24 17:12:32 -05:00
firestar5683 aaf1061111 Sportage Exception 2026-09-23 19:06:59 -05:00
firestar5683 79c61f479a Update starpilot_version.py 2026-09-22 23:49:59 -05:00
firestar5683 2a12dbd0ad Update starpilot_version.py 2026-09-22 23:49:36 -05:00
firestar5683 f0cac32351 Update starpilot_version.py 2026-09-22 23:49:29 -05:00
firestar5683 5bc666676a Tunes 2026-09-22 22:36:20 -05:00
firestar5683 2a528414ed Ray Pedal Path 2026-09-22 16:44:12 -05:00
firestar5683 ecda0c61d9 Corolla 2026-09-22 16:18:40 -05:00
firestar5683 399a40ca22 build 2026-09-22 16:02:08 -05:00
firestar5683 e47133be1a AOL No default 2026-09-22 15:55:47 -05:00
firestar5683 5ce64a49a8 Reduce first settings open cost and repeated vehicle catalog parsing 2026-09-22 14:38:35 -05:00
firestar5683 4c47955498 Keep navigation animation timing consistent under onroad load 2026-09-22 14:38:35 -05:00
firestar5683 fab2494f8e Batch small UI lane and road edge projections 2026-09-22 14:38:35 -05:00
firestar5683 96a75ba908 link commits 2026-09-22 13:45:25 -05:00
firestar5683 3a41fe663a preap 2026-09-22 13:27:02 -05:00
firestar5683 678af78347 Settle UI page transitions exactly and keep animation timing consistent 2026-09-22 09:26:12 -05:00
firestar5683 9d8a523471 Upload only the visible small UI camera region 2026-09-22 09:26:12 -05:00
firestar5683 b7cd0caff2 Keep UI scheduling below planning and stabilize speed limit pulses 2026-09-22 09:26:12 -05:00
firestar5683 cdc6b3bd68 Reduce onroad UI work and move parameter refresh off render thread 2026-09-22 09:26:12 -05:00
firestar5683 7f0c5673b4 pre-ap 2026-09-22 08:37:31 -05:00
firestar5683 2a13cc7fe2 aussie 2026-09-21 22:53:00 -05:00
firestar5683 7c6038fe28 build 2026-09-21 21:26:32 -05:00
firestar5683 5925aecd5b Flight Delayed 2026-09-21 21:24:05 -05:00
firestar5683 e6390c32e1 Audit every fleet route against current controller and safety hooks 2026-09-21 21:13:05 -05:00
firestar5683 1590a5cc2b Exercise AOL state machine across fleet platform fixtures 2026-09-21 21:08:27 -05:00
firestar5683 78d412d1d0 Add strict offline fleet TX safety audit core 2026-09-21 15:26:25 -05:00
firestar5683 d34a756929 test: count AOL authorization before replay TX checks 2026-09-21 15:16:17 -05:00
whoisdomi f15a1974d5 Ioniq 6 turn blips 2026-09-19 21:01:03 -05:00
whoisdomi 08139a021a Car Date/Time fallback when gps/wifi not available 2026-09-19 21:01:02 -05:00
whoisdomi fbe982f47b Ioniq 6 Date/Time DBC Signal
Added date/time signal from Ioniq 6 can
2026-09-19 21:01:01 -05:00
firestarsdog 5e6e978438 Gen2 Bolt HSA Fix? Maybe?
0x315 : Mode 1 when not engaged, not Mode 9
2026-09-19 18:25:59 -04:00
firestarsdog b990a776b2 Curve radial menu corner gradient 2026-09-18 22:26:26 -04:00
firestarsdog 88cbf88756 TV Set 2026-09-18 22:17:55 -04:00
whoisdomi 373c411baa Model Stuff 2026-09-18 19:47:39 -05:00
Prabhaav Pillai 990e68804c fix recording and revert nav tab 2026-09-18 20:04:20 -04:00
firestarsdog b295a57281 mici ux 2026-09-18 18:12:53 -04:00
627 changed files with 100571 additions and 4750 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Fleet controller safety
on:
workflow_dispatch:
pull_request:
paths:
- 'opendbc_repo/**'
- 'selfdrive/car/**'
- 'starpilot/car/**'
- 'starpilot/controls/**'
- 'cereal/**'
- '.github/workflows/fleet_safety.yaml'
permissions:
contents: read
jobs:
harness:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python -m pip install -r selfdrive/car/tests/fleet_requirements.txt
- name: Audit accounting and runner failures
run: >-
python -m pytest --noconftest -o addopts='' -q
selfdrive/car/tests/test_fleet_safety_core.py
selfdrive/car/tests/test_fleet_safety_runner.py
opendbc_repo/opendbc/safety/tests/safety_replay/test_replay_drive.py
recorded_fleet:
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python -m pip install -r selfdrive/car/tests/fleet_requirements.txt
- name: Current controllers against freshly compiled release safety
run: >-
python -m selfdrive.car.tests.fleet_safety --all --release
--shard-count 8 --shard-index ${{ matrix.shard }}
--out selfdrive/car/tests/fleet_results/ci
- uses: actions/upload-artifact@v4
if: always()
with:
name: fleet-safety-${{ matrix.shard }}
path: |
selfdrive/car/tests/fleet_results/ci/**/*.json
selfdrive/car/tests/fleet_results/ci/**/*.log
+2
View File
@@ -237,6 +237,8 @@ struct StarPilotPlan @0xf98d843bfd7004a3 {
cscLearnedLatAccel @40 :Float32; # learned comfort at the current curvature, before margin
cscBindingDistance @41 :Float32; # distance to the horizon point setting the target, m
approachStopLength @42 :Float32; # pre-commit distance to a detected stop, m; 0 when off
slcPresentedSpeedLimitSource @43 :Text; # source of the shown accepted or pending posted limit
slcIsLimitingMaxSet @44 :Bool; # SLC target is below the configured Max Set
}
struct StarPilotRadarState @0xb86e6369214c01c8 {
Binary file not shown.
+7 -1
View File
@@ -198,7 +198,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"AggressiveJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"AlertVolumeControl", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"AllowImpossibleAcceleration", {PERSISTENT, BOOL, "0", "0", 3}},
{"AlwaysOnLateral", {PERSISTENT, BOOL, "1", "0", 0, SETTINGS_SIMPLE}},
{"AlwaysOnLateral", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
{"AlwaysOnLateralLKAS", {PERSISTENT, BOOL, "1", "0", 2}},
{"ApiCache_DriveStats", {PERSISTENT, JSON, "{}", "{}"}},
{"AutomaticallyDownloadModels", {PERSISTENT, BOOL, "1", "0", 1}},
@@ -389,6 +389,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ForceStopDistanceOffset", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
{"ForceStandstill", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"FordLKASButtonControlMigrated", {PERSISTENT, BOOL, "0", "0"}},
{"SonataHybridLKASButtonControlMigrated", {PERSISTENT, BOOL, "0", "0"}},
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
// These Ford curvature tuning concepts descend from BluePilot bp-7.0. StarPilot's key names and
// settings integration are local; see /CREDITS.md and /THIRD_PARTY_NOTICES.md for provenance.
@@ -702,6 +703,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"StandbyMode", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
{"StandbyWakeButton", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"StandbyButtonPressTime", {CLEAR_ON_MANAGER_START | DONT_LOG, INT, "0", "0"}},
{"ScreenOffToggleCounter", {CLEAR_ON_MANAGER_START | DONT_LOG, INT, "0", "0"}},
{"StandbyWakeEngage", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
{"StandbyWakeDisengage", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
{"StandbyWakeInfoAlert", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
@@ -727,6 +729,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SteerRatio", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerRatioStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"EnableTorqueBarWidget", {PERSISTENT, BOOL, "1", "0", 0}},
{"StingerObjectShadow", {PERSISTENT, BOOL, "0", "0", 3}},
{"StockConfidenceBallWidget", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
{"StockDongleId", {PERSISTENT, STRING, "", ""}},
{"StopAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
@@ -740,8 +743,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
{"SubaruSNGManualParkingBrake", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruStopStartOff", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruAvhStartup", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruRedneckCruise", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
{"TeslaAOLDisengageOnBrake", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
{"TeslaAOLScreenTap", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
{"TeslaCoopSteering", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"TetheringEnabled", {PERSISTENT, INT, "0", "0", 0}},
Binary file not shown.
+2 -1
View File
@@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 453 Supported Cars
# 454 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -265,6 +265,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte 2022-23">Buy Here</a></sub></details>|||
|Kia|K4 (with HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (with HDA II) 2025">Buy Here</a></sub></details>|||
|Kia|K4 (without HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (without HDA II) 2025">Buy Here</a></sub></details>|||
|Kia|K4 (without HDA II) 2026|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (without HDA II) 2026">Buy Here</a></sub></details>|||
|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 2021-24">Buy Here</a></sub></details>|||
|Kia|K5 (without HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 (without HDA II) 2025">Buy Here</a></sub></details>|||
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 Hybrid 2020-22">Buy Here</a></sub></details>|||
+86
View File
@@ -0,0 +1,86 @@
# Fleet offline audit — September 21, 2026
This is a first-pass fault and coverage inventory, not fleet driving clearance.
No hardware was accessed and no controller or safety policy was changed by this
audit. Other work was concurrently modifying the checkout; per-case source and
library hashes identify the tested implementations.
The full debug-library run visited all 345 platforms. It evaluated both recorded
and AOL-main scenarios for 287 registered routes, plus 108 missing-route entries:
| Result | Cases |
|---|---:|
| Pass within the stated scope | 103 |
| Failed checks, requiring triage | 123 |
| Missing coverage | 288 |
| Could not evaluate | 168 |
| Total | 682 |
These are case counts, not numbers of unsafe cars. Missing coverage includes all
108 platforms without routes and segments without active transitions. Evaluation
errors include unavailable recordings and identities that do not match the
registered platform after the existing fingerprint migration. Old logs must be
normalized explicitly, not quietly substituted for another car.
Full local evidence is under
`selfdrive/car/tests/fleet_results/full_fleet/results.json`, with per-shard build,
case and worker logs. Generated evidence is ignored by Git.
The follow-up full release-library run also completed 682 cases: **102 pass,
153 failed, 289 uncovered, 138 evaluation errors**. Evidence is under
`selfdrive/car/tests/fleet_results/release_fleet_checked/results.json`.
The two batches had different download availability and ran against a changing
working tree; their count difference is not an isolated debug-versus-release
experiment. Both correctly exit nonzero. The release batch is not fleet clearance.
## Confirmed distinctions from failure triage
**Hyundai Custin — controller/safety capability mismatch.** The registered
segment `0bbe367c98fa1538/2023-09-16--00-16-49/2` contains 600 camera-bus
messages at 0x53e, all eight bytes, none six. `CarInterfaceBase.get_starpilot_params`
in `opendbc_repo/opendbc/car/interfaces.py` enables HAS_LKAS12 by address alone.
Hyundai `CarState.update` and `CarController.update` then produce six-byte LKAS12
replacements. In `opendbc_repo/opendbc/safety/modes/hyundai.h`,
`hyundai_rx_all_hook` only enables replacement after receiving a six-byte camera
message, and `hyundai_tx_hook` correctly rejects the unsolicited replacement.
Both scenarios reject 5,799 such packets. A fresh-library probe also reproduced
the six-byte/eight-byte distinction. Repair requires a controller capability and
parser regression test; do not broaden the safety allowlist to hide the mismatch.
**Honda Civic Bosch — incompatible historical control requests.** All 5,997
recorded requests in the inspected 2020 fixture have enabled/latActive/longActive
false but resume true; all 6,000 cruise-state CAN messages are disabled. Current
controller output is RES_ACCEL at 0x296, which current safety correctly blocks.
The AOL probe suppresses resume and passes. Investigate historical command/schema
semantics before calling this a current steering defect.
**Ford Escape — mid-segment initialization artifact.** The first cruise-enabled
0x165 enables controls, but the immediately following 0x202 reaches
`speed_mismatch_check` before safety has nonzero speed history. Controls are
revoked; cruise stays enabled for the entire segment so `pcm_cruise_check` sees
no new rising edge. Relay health remains good. This reproduces with both recorded
and default alternative experience. A two-second scoring warmup does not repair
the latch. This needs recorded preroll/initialization coverage, not force-setting
`controls_allowed` or changing vehicle safety.
## Tesla and AOL scope
In the full debug-library run, the Model 3 route and the second Model Y route
passed both scenarios. The first Model Y route lacked a lateral transition;
its AOL case also lacked requested steering under AOL-only safety permission.
Model X was uncovered as a current dashcam-only configuration. The Model S HW1
and Pre-AP entries have no registered routes. None of these findings reproduces
or disproves the exact hackathon oscillation without its trace.
The separate actual StarPilotCard synthetic-input suite passed 964 checks with
72 explicit active-sequence gaps. All 345 disabled configurations were checked;
309 platforms completed both active modes. These tests check state-machine gates
and stable sequences, not the entire selfdrived-to-Panda feedback loop.
The test-harness regressions pass 34 tests. They cover pre-hook AOL authorization,
strict configuration/bus routing, rejected active packets, expected negative
checks, empty activity, worker crashes, stale reports and build failures.
See `FLEET_SAFETY_TESTING.md` for commands, CI scope and limitations. The workflow
has been added locally but not published or run on GitHub, and branch protection
has not been changed. The full fleet is not green.
+112
View File
@@ -0,0 +1,112 @@
# Offline fleet controller and safety checks
The runner in `selfdrive/car/tests/fleet_safety.py` enumerates every platform in
the current checkout and uses `opendbc.car.tests.routes`. It runs current
CarInterface/CarState/CarController code against recorded CAN and actuator
requests, then checks each newly generated CAN packet with freshly compiled
current safety hooks. It never connects to a Panda or starts vehicle processes.
Run from the repository root with the repository Python environment:
```sh
python -m selfdrive.car.tests.fleet_safety --inventory
python -m selfdrive.car.tests.fleet_safety --platform TESLA_MODEL_Y --release
python -m selfdrive.car.tests.fleet_safety --all --release
```
An isolated dependency set is in `selfdrive/car/tests/fleet_requirements.txt`.
The runner needs a C compiler. It does not use a previously staged libsafety.
Without `--release`, the safety library enables ALLOW_DEBUG, like the existing
safety unit tests. Release checks are needed as well: a debug-only hook must not
be mistaken for an available production configuration. Release host builds retain
unused-variable warnings without treating that specific diagnostic as an error.
For parallel runs, use separate output directories:
```sh
python -m selfdrive.car.tests.fleet_safety --all --release \
--shard-count 8 --shard-index 0 --out selfdrive/car/tests/fleet_results/shard_0
```
Run indices 0 through 7. Each has its own library copies, worker processes,
parameter namespace, logs and results. `--local-log` allows a local rlog for one
explicitly selected platform. Route IDs and old fingerprint aliases must match;
the harness does not silently treat another vehicle's log as that platform.
## What is checked
- Recorded commands under the current default feature configuration.
- A separate AOL MAIN-availability controller/safety probe, using recorded
actuator values with longitudinal requests and cruise button requests off.
- Actual per-Panda safety model, CP/FPCP safety-param OR, alternative-experience
OR, and strict four-bus routing, matching production configuration assembly.
- Incoming CAN, current CarState validity and safety receive health.
- Every emitted TX, including inactive-state packets; unexpected rejection fails.
- Pre-hook normal/AOL/longitudinal permissions, so a rejection that revokes
authorization cannot disappear from the failure accounting.
- Active requests, accepted active TX, engagement transitions, and AOL-only
safety authorization coverage. Sparse commands and absent transitions cannot
qualify as complete coverage.
There is a two-second unscored fixture startup interval. Controller and safety
history still receive messages during it. The harness does not force safety
authorization or clear a relay fault to manufacture a passing result.
## Results are deliberately strict
`pass` means the case satisfied these specific checks and coverage requirements.
`failed` means a hook/health check failed and needs investigation. `uncovered`
means the scenario was not demonstrated, including missing routes, dashcam-only
interfaces and segments without transitions. `error` means the case could not be
evaluated, such as download failure or mismatched fixture identity. Anything
other than pass makes the command exit nonzero. Existing `non_tested_cars`
exemptions remain visible coverage gaps.
Reports include frame counters, bounded rejected packet evidence, source hashes,
effective safety configurations and build provenance. Worker results carry a
unique execution ID; a crash, stale result or inconsistent exit code cannot be
reused as a pass. JSON and logs live under the ignored `fleet_results` directory.
The AOL probe is **not** a complete simulation of StarPilotCard, selfdrived,
controls mismatch handling or a vehicle ECU. Recorded commands may also reflect
historical settings different from current defaults. A blocked historical resume
request is not automatically a steering bug. Investigate each failure before
changing code. Do not widen safety permissions to make tests green.
## Continuous integration and remaining coverage
`starpilot/controls/tests/test_fleet_aol.py` separately exercises the actual
StarPilotCard state machine with isolated synthetic inputs for every platform.
It tests feature-off behavior, steady engagement, AOL-only operation, brake
pause, native/StarPilot immediate-disable alerts, calibration and gear gates.
It uses empty firmware/fingerprint fixtures, so optional vehicle configurations
are not covered by these sequences. Run it with a compatible built host runtime:
```sh
python -m pytest --noconftest -o addopts='' -q starpilot/controls/tests/test_fleet_aol.py
```
The first run passed 964 checks and explicitly skipped 72 active sequences:
309 platforms exercised both active modes; 36 platforms had two gaps each
(30 dashcam-only, one notCar, four Volvo policy exclusions, one Pre-AP external
authorization dependency). All 345 feature-off checks passed. A separate
Pre-AP authorization input boundary test is synthetic, not proof of actual
Panda authorization. These tests were run against the current working tree,
including concurrent Pre-AP changes; they do not certify an earlier commit.
The lightweight CI workflow below does not build the native runtime required
by this separate state-machine suite.
`.github/workflows/fleet_safety.yaml` adds harness tests and eight release-mode
recorded-route shards on relevant pull requests and manual runs. Missing coverage
is not converted to a skip or allowed failure. The current fleet is not green;
this workflow will expose that fact. It has not been executed on GitHub from this
local task. Requiring it for merge also needs repository branch protection; a
workflow file alone does not change repository settings.
As of the first September 21 inventory there are 345 platforms, 287 registered
routes across 237 platforms, and 108 platforms with no registered route. A route
entry does not guarantee valid, downloadable logs or all necessary maneuvers.
Every optional harness, longitudinal mode, safety parameter, firmware generation
and AOL configuration still needs explicit coverage. Offline checks reduce
blind spots; they do not certify every physical vehicle or reproduce an incident
whose CAN trace was not retained.
+4
View File
@@ -133,6 +133,10 @@ function launch {
# start manager
cd system/manager
if [ -f /AGNOS ]; then
timeout 15s python3 "$DIR/tools/galaxy_bluetooth/settings_backup.py" || echo "Galaxy settings backup unavailable"
fi
sp_launch_timing "launch_param_migrations_start"
if ! python3 ./launch_param_migrations.py; then
echo "Launch param migrations failed; continuing boot."
+2 -2
View File
@@ -21,11 +21,11 @@ fi
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.6.20"
export AGNOS_VERSION="19.8.1"
fi
if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then
export AGNOS_ACCEPTED_VERSIONS="$AGNOS_VERSION"
export AGNOS_ACCEPTED_VERSIONS="19.8.1 19.8.2"
fi
export STAGING_ROOT="/data/safe_staging"
+2 -1
View File
@@ -1,6 +1,6 @@
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. --->
# Support Information for 518 Known Cars
# Support Information for 519 Known Cars
|Make|Model|Package|Support Level|
|---|---|---|:---:|
@@ -287,6 +287,7 @@
|Kia|Forte Non-SCC 2021|No Smart Cruise Control (Non-SCC)|[Community](community)|
|Kia|K4 (with HDA II) 2025|Smart Cruise Control (SCC)|[Upstream](#upstream)|
|Kia|K4 (without HDA II) 2025|Smart Cruise Control (SCC)|[Upstream](#upstream)|
|Kia|K4 (without HDA II) 2026|Smart Cruise Control (SCC)|[Upstream](#upstream)|
|Kia|K5 2021-24|Smart Cruise Control (SCC)|[Upstream](#upstream)|
|Kia|K5 (without HDA II) 2025|Smart Cruise Control (SCC)|[Upstream](#upstream)|
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|[Upstream](#upstream)|
+27 -11
View File
@@ -4,7 +4,7 @@ from opendbc.can import CANPacker
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, structs
from opendbc.car.lateral import ISO_LATERAL_ACCEL, apply_std_steer_angle_limits
from opendbc.car.ford import fordcan
from opendbc.car.ford.values import CarControllerParams, FordFlags
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
# This Ford extension boundary substantially adapts BluePilot bp-7.0 work. See the root CREDITS.md
# (including Alan Polk's d0aac605f and db2bdff05) and THIRD_PARTY_NOTICES.md.
@@ -64,13 +64,29 @@ def apply_ford_curvature_limits(apply_curvature, apply_curvature_last, current_c
return apply_curvature
def apply_creep_compensation(accel: float, v_ego: float) -> float:
def apply_creep_compensation(accel: float, v_ego: float, car_fingerprint: str, *, standstill: bool, stopping: bool) -> float:
if car_fingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not (standstill and stopping):
return accel
creep_accel = np.interp(v_ego, [1., 3.], [0.6, 0.])
creep_accel = np.interp(accel, [0., 0.2], [creep_accel, 0.])
accel -= creep_accel
return float(accel)
def apply_brake_rate_limit(accel: float, gas: float, accel_last: float, v_ego: float, car_fingerprint: str,
*, stopping: bool, standstill: bool, fcw: bool) -> tuple[float, float]:
mild_mach_e_braking = (
car_fingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and
-1.0 < gas < 0.0 and v_ego >= 5.0 and not (stopping or standstill or fcw)
)
if mild_mach_e_braking:
accel = max(accel, min(accel_last, 0.0) - 2.0 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL)
gas = accel
else:
accel = max(accel, accel_last - 3.5 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL)
return float(accel), float(gas)
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
@@ -165,7 +181,7 @@ class CarController(CarControllerBase):
can_sends.append(starpilot_fordcan.create_lat_ctl2_msg(
self.packer, self.CAN, 1 if lateral.active else 0,
lateral.ramp_type, lateral.precision_type,
-lateral.curvature, -lateral.curvature_rate, counter))
-lateral.curvature, -lateral.curvature_rate, counter, -lateral.path_angle))
else:
can_sends.append(starpilot_fordcan.create_lat_ctl_msg(
self.packer, self.CAN, lateral.active,
@@ -181,16 +197,17 @@ class CarController(CarControllerBase):
if self.CP.openpilotLongitudinalControl and (self.frame % CarControllerParams.ACC_CONTROL_STEP) == 0:
accel = actuators.accel
gas = accel
stopping = actuators.longControlState == LongCtrlState.stopping
if CC.longActive:
# Compensate for engine creep at low speed.
# Either the ABS does not account for engine creep, or the correction is very slow
# TODO: verify this applies to EV/hybrid
accel = apply_creep_compensation(accel, CS.out.vEgo)
accel = apply_creep_compensation(accel, CS.out.vEgo, self.CP.carFingerprint,
standstill=CS.out.standstill, stopping=stopping)
# The stock system has been seen rate limiting the brake accel to 5 m/s^3,
# however even 3.5 m/s^3 causes some overshoot with a step response.
accel = max(accel, self.accel - (3.5 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL))
accel, gas = apply_brake_rate_limit(
accel, gas, self.accel, CS.out.vEgo, self.CP.carFingerprint,
stopping=stopping, standstill=CS.out.standstill,
fcw=fcw_alert or CS.out.stockFcw or CS.out.stockAeb,
)
accel = float(np.clip(accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
gas = float(np.clip(gas, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
@@ -210,7 +227,6 @@ class CarController(CarControllerBase):
elif accel_pitch_compensated < 0.0:
self.brake_request = True
stopping = CC.actuators.longControlState == LongCtrlState.stopping
# TODO: look into using the actuators packet to send the desired speed
can_sends.append(fordcan.create_acc_msg(self.packer, self.CAN, CC.longActive, gas, accel, stopping, self.brake_request, v_ego_kph=V_CRUISE_MAX))
+15 -1
View File
@@ -29,6 +29,9 @@ class CarState(CarStateBase):
self.distance_button = 0
self.lc_button = 0
self.cancel_button = False
self.cancel_resume_pressed = False
self.cancel_resume_is_cancel = False
self.lkas_available = False
self.lateral_motion_control = None
self.lateral_control_status = None
@@ -156,6 +159,14 @@ class CarState(CarStateBase):
prev_lc_button = self.lc_button
self.distance_button = cp.vl["Steering_Data_FD1"]["AccButtnGapTogglePress"]
self.lc_button = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"])
prev_cancel_button = self.cancel_button
cancel_resume_pressed = bool(cp.vl["Steering_Data_FD1"]["CcAslButtnCnclResPress"])
if cancel_resume_pressed and not self.cancel_resume_pressed:
self.cancel_resume_is_cancel = ret.cruiseState.available and ret.cruiseState.enabled
self.cancel_resume_pressed = cancel_resume_pressed
self.cancel_button = bool(cp.vl["Steering_Data_FD1"]["CcAslButtnCnclPress"]) or (
cancel_resume_pressed and self.cancel_resume_is_cancel
)
# lock info
ret.doorOpen = any([cp.vl["BodyInfo_3_FD1"]["DrStatDrv_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatPsngr_B_Actl"],
@@ -183,10 +194,13 @@ class CarState(CarStateBase):
except KeyError:
self.lateral_motion_control = None
ret.buttonEvents = [
button_events = [
*create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}),
*create_button_events(self.lc_button, prev_lc_button, {1: ButtonType.lkas}),
]
if self.CP.openpilotLongitudinalControl:
button_events += create_button_events(self.cancel_button, prev_cancel_button, {1: ButtonType.cancel})
ret.buttonEvents = button_events
fp_ret = custom.StarPilotCarState.new_message()
fp_ret.brakeLights = ret.brakePressed
+5 -1
View File
@@ -8,7 +8,7 @@ from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.carstate import CarState
from opendbc.car.ford.fordcan import CanBus
from opendbc.car.ford.radar_interface import RadarInterface
from opendbc.car.ford.values import CarControllerParams, DBC, Ecu, FordFlags, RADAR, FordSafetyFlags
from opendbc.car.ford.values import CAR, CarControllerParams, DBC, Ecu, FordFlags, RADAR, FordSafetyFlags
from opendbc.car.interfaces import CarInterfaceBase
TransmissionType = structs.CarParams.TransmissionType
@@ -40,6 +40,8 @@ class CarInterface(CarInterfaceBase):
ret.longitudinalTuning.kiBP = [0.]
ret.longitudinalTuning.kiV = [0.5]
ret.longitudinalTuning.kpV = [0.]
if candidate == CAR.FORD_MUSTANG_MACH_E_MK1:
ret.longitudinalTuning.kiV = [0.3]
if not ret.radarUnavailable and DBC[candidate][Bus.radar] == RADAR.DELPHI_MRR:
# average of 33.3 Hz radar timestep / 4 scan modes = 60 ms
@@ -63,6 +65,8 @@ class CarInterface(CarInterfaceBase):
if ret.flags & FordFlags.CANFD:
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.CANFD.value
if candidate == CAR.FORD_MUSTANG_MACH_E_MK1:
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.MACH_E_CURVATURE.value
# TRON (SecOC) platforms are not supported
# LateralMotionControl2, ACCDATA are 16 bytes on these platforms
@@ -6,15 +6,16 @@ from hypothesis import settings, given, strategies as st
from parameterized import parameterized
import pytest
from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.can import CANPacker
from opendbc.car import Bus, gen_empty_fingerprint, structs
from opendbc.can import CANPacker, CANParser
from opendbc.car.ford import fordcan
from opendbc.car.ford.carcontroller import FordStockCruiseButton
from opendbc.car.ford.carcontroller import CarController, FordStockCruiseButton, apply_brake_rate_limit, apply_creep_compensation
from opendbc.car.ford.carstate import CarState
from opendbc.car.gps import FORD_MACH_E_GPS_MESSAGES, get_car_gps_config, parse_ford_can_gps
from opendbc.car.structs import CarParams
from opendbc.car.structs import CarParams, CarState as CarStateStruct
from opendbc.car.fw_versions import build_fw_dict
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, FordSafetyFlags, get_platform_codes, match_vin_to_car
from opendbc.car.ford.values import CAR, DBC, FW_QUERY_CONFIG, FW_PATTERN, FordSafetyFlags, get_platform_codes, match_vin_to_car
from opendbc.car.ford.fingerprints import FW_VERSIONS
Ecu = CarParams.Ecu
@@ -38,6 +39,135 @@ def test_stock_cruise_button_ignores_press_with_cruise_master_off():
assert button.update(True, cruise_available=False, cruise_enabled=False) == (False, False)
@pytest.mark.parametrize("op_long", (False, True))
@pytest.mark.parametrize("standstill", (False, True))
@pytest.mark.parametrize("cruise_status", (4, 5))
@pytest.mark.parametrize("switch", ("CcAslButtnCnclResPress", "CcAslButtnCnclPress"))
def test_ford_cancel_event_does_not_require_pcm_disengagement(op_long, standstill, cruise_status, switch):
CP = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, gen_empty_fingerprint(), [], op_long, False, False, None)
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
for frame, (pressed, status) in enumerate(((True, cruise_status), (True, 3), (False, 3)), start=1):
messages = [
packer.make_can_msg("Steering_Data_FD1", fordcan.CanBus(CP).main, {switch: int(pressed)}),
packer.make_can_msg("EngBrakeData", fordcan.CanBus(CP).main, {"CcStat_D_Actl": status}),
packer.make_can_msg("DesiredTorqBrk", fordcan.CanBus(CP).main, {"VehStop_D_Stat": int(standstill)}),
]
parsers[Bus.pt].update([(frame * 100_000_000, messages)])
ret, _ = state.update(parsers, None)
assert ret.standstill == standstill
expected = [(CarStateStruct.ButtonEvent.Type.cancel, pressed)] if op_long and frame != 2 else []
assert [(event.type, event.pressed) for event in ret.buttonEvents] == expected
@pytest.mark.parametrize("initial_status", (0, 3))
def test_ford_resume_does_not_become_cancel_when_pcm_engages(initial_status):
CP = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, gen_empty_fingerprint(), [], True, False, False, None)
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
for frame, (pressed, status) in enumerate(((True, initial_status), (True, 4), (False, 4)), start=1):
messages = [
packer.make_can_msg("Steering_Data_FD1", fordcan.CanBus(CP).main, {"CcAslButtnCnclResPress": int(pressed)}),
packer.make_can_msg("EngBrakeData", fordcan.CanBus(CP).main, {"CcStat_D_Actl": status}),
]
parsers[Bus.pt].update([(frame * 100_000_000, messages)])
ret, _ = state.update(parsers, None)
assert not ret.buttonEvents
def test_mach_e_does_not_apply_engine_creep_compensation():
for accel in (-1.0, -0.1, 0.0, 0.1):
assert apply_creep_compensation(accel, 0.5, CAR.FORD_MUSTANG_MACH_E_MK1,
standstill=False, stopping=False) == accel
assert apply_creep_compensation(0.0, 0.0, CAR.FORD_MUSTANG_MACH_E_MK1,
standstill=True, stopping=True) == -0.6
assert apply_creep_compensation(0.0, 0.5, CAR.FORD_F_150_MK14,
standstill=False, stopping=False) == -0.6
@pytest.mark.parametrize("last", (0.0, 0.35, 0.8))
def test_mach_e_mild_brake_transition_shapes_regen_and_brake_together(last):
accel, gas = apply_brake_rate_limit(-0.5, -0.5, last, 11.0, CAR.FORD_MUSTANG_MACH_E_MK1,
stopping=False, standstill=False, fcw=False)
assert accel == pytest.approx(-0.04)
assert gas == accel
@pytest.mark.parametrize("accel,last", ((-0.2, -0.5), (-0.5, -0.48), (-0.8, -0.4)))
def test_mach_e_mild_brake_ramp_does_not_delay_release(accel, last):
brake, gas = apply_brake_rate_limit(accel, accel, last, 11.0, CAR.FORD_MUSTANG_MACH_E_MK1,
stopping=False, standstill=False, fcw=False)
assert brake == pytest.approx(max(accel, last - 0.04))
assert gas == brake
@pytest.mark.parametrize("accel,speed,stopping,standstill,fcw", (
(-3.5, 11.0, False, False, False),
(-1.0, 11.0, False, False, False),
(-0.5, 4.9, False, False, False),
(-0.5, 11.0, True, False, False),
(-0.5, 11.0, False, True, False),
(-0.5, 11.0, False, False, True),
(0.2, 11.0, False, False, False),
))
def test_mach_e_strong_braking_stops_and_warnings_keep_original_ramp(accel, speed, stopping, standstill, fcw):
brake, gas = apply_brake_rate_limit(accel, accel, 0.0, speed, CAR.FORD_MUSTANG_MACH_E_MK1,
stopping=stopping, standstill=standstill, fcw=fcw)
assert brake == pytest.approx(max(accel, -0.07))
assert gas == accel
@pytest.mark.parametrize("fingerprint", [car for car in CAR if car != CAR.FORD_MUSTANG_MACH_E_MK1])
def test_other_fords_keep_original_brake_requests_and_integral_gain(fingerprint):
brake, gas = apply_brake_rate_limit(-0.5, -0.5, 0.35, 11.0, fingerprint,
stopping=False, standstill=False, fcw=False)
assert brake == pytest.approx(0.28)
assert gas == -0.5
CP = CarInterface.get_params(fingerprint, gen_empty_fingerprint(), [], True, False, False, None)
assert list(CP.longitudinalTuning.kiV) == [0.5]
def test_mach_e_integral_gain_preserves_delay_and_braking_authority():
CP = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, gen_empty_fingerprint(), [], True, False, False, None)
assert list(CP.longitudinalTuning.kiV) == pytest.approx([0.3])
assert CP.longitudinalActuatorDelay == pytest.approx(0.15)
assert CarInterface.get_pid_accel_limits(CP, 11.0, 20.0)[0] == -3.5
def test_mach_e_can_brake_handoff_and_driver_override(monkeypatch):
monkeypatch.setattr("opendbc.car.ford.carcontroller.FordLateralController",
lambda _: SimpleNamespace(update_inputs=lambda: None))
monkeypatch.setattr(fordcan, "create_acc_ui_msg", lambda *args: (0, b"", 0))
monkeypatch.setattr(fordcan, "create_lkas_ui_msg", lambda *args: (0, b"", 0))
CP = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, gen_empty_fingerprint(), [], True, False, False, None)
controller = CarController(DBC[CP.carFingerprint], CP)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ACCDATA", 50)], 0)
CC = structs.CarControl.new_message()
CC.enabled = True
CC.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid
CS = SimpleNamespace(out=structs.CarState.new_message(), acc_tja_status_stock_values={"Tja_D_Stat": 0})
CS.out.vEgo = 11.0
for frame, active, request, expected in ((2, True, -0.5, -0.04), (4, False, 0.0, 0.0),
(8, True, -0.5, -0.04), (14, True, -3.5, -0.11)):
controller.frame = frame
CC.longActive = active
CC.actuators.accel = request
output, messages = controller.update(CC.as_reader(), CS, frame * 10_000_000, None)
parser.update([(frame * 10_000_000, [msg for msg in messages if msg[0] == 0x186])])
values = parser.vl["ACCDATA"]
assert output.accel == pytest.approx(expected)
assert values["AccBrkTot_A_Rq"] == pytest.approx(expected, abs=0.01)
assert values["AccPrpl_A_Rq"] == pytest.approx(expected if active and request > -1.0 else -5.0, abs=0.01)
assert values["Cmbb_B_Enbl"] == int(active)
assert values["AccResumEnbl_B_Rq"] == int(active)
ECU_ADDRESSES = {
Ecu.eps: 0x730, # Power Steering Control Module (PSCM)
Ecu.abs: 0x760, # Anti-Lock Brake System (ABS)
@@ -192,10 +322,12 @@ def test_mach_e_longitudinal_toggle_controls_stock_acc_selection():
assert not stock.openpilotLongitudinalControl
assert stock.pcmCruise
assert not (stock.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
assert stock.safetyConfigs[-1].safetyParam & FordSafetyFlags.MACH_E_CURVATURE
assert enhanced.alphaLongitudinalAvailable
assert enhanced.openpilotLongitudinalControl
assert enhanced.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL
assert enhanced.safetyConfigs[-1].safetyParam & FordSafetyFlags.MACH_E_CURVATURE
def test_mach_e_can_gps_decode():
+1
View File
@@ -50,6 +50,7 @@ class FordSafetyFlags(IntFlag):
LONG_CONTROL = 1
CANFD = 2
LKA_STEERING = 4
MACH_E_CURVATURE = 8
class FordFlags(IntFlag):
+1 -1
View File
@@ -1197,7 +1197,7 @@ class CarController(CarControllerBase):
# cannot linger after a disengage or main-off event.
if should_send_bolt_acc_pedal_friction:
can_sends.append(gmcan.create_friction_brake_command(
self.packer_ch, friction_brake_bus, experiment_brake, idx, bolt_acc_pedal_friction_main_on,
self.packer_ch, friction_brake_bus, experiment_brake, idx, bolt_acc_pedal_friction_main_on and CC.longActive,
near_stop, at_full_stop, self.CP))
if self.CP.carFingerprint not in CC_ONLY_CAR:
friction_brake_bus = get_friction_brake_bus(self.CP)
+60
View File
@@ -8,6 +8,7 @@ from typing import Any
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.ford.values import CAR as FORD_CAR
from opendbc.car.gm.values import CAR as GM_CAR
from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN_CAR
CarGpsSample = dict[str, Any]
@@ -124,12 +125,66 @@ def parse_chevrolet_bolt_can_gps(position: Mapping[str, float]) -> CarGpsSample
}
def parse_volkswagen_taos_can_gps(position: Mapping[str, float], motion: Mapping[str, float],
altitude: Mapping[str, float], status: Mapping[str, float]) -> CarGpsSample | None:
packet_ids = (position["GNSS_Nachrichtenpaket_ID1"], motion["GNSS_Nachrichtenpaket_ID2"],
altitude["GNSS_Nachrichtenpaket_ID4"], status["GNSS_Nachrichtenpaket_ID5"])
if len(set(packet_ids)) != 1:
return None
timestamp_ms = int(status["GNSS_UTC_Zeit"]) * 1000
if timestamp_ms == 0:
return None
latitude = float(position["GNSS_LatitudeMagnitude"])
longitude = float(position["GNSS_LongitudeMagnitude"])
coordinates_valid = (math.isfinite(latitude) and math.isfinite(longitude) and
0.0 <= latitude <= 90.0 and 0.0 <= longitude <= 180.0 and (latitude != 0.0 or longitude != 0.0))
if coordinates_valid:
latitude *= -1.0 if position["GNSS_LatitudeSouth"] else 1.0
longitude *= -1.0 if position["GNSS_LongitudeWest"] else 1.0
else:
latitude = longitude = 0.0
satellite_count = int(status["GNSS_Genutzte_Satelliten"])
hemisphere_validated = position["GNSS_LatitudeSouth"] == 0 and position["GNSS_LongitudeWest"] == 1
has_fix = (coordinates_valid and hemisphere_validated and position["GNSS_PositionStatus"] == 3 and status["GNSS_Empfaenger_Status"] == 1 and
bool(status["GNSS_GPS_in_Nutzung"] or status["GNSS_GLONASS_in_Nutzung"]) and 4 <= satellite_count <= 31)
speed = float(motion["GNSS_Speed"])
bearing = float(motion["GNSS_Bearing"])
speed_valid = math.isfinite(speed) and 0.0 <= speed <= 127.5
bearing_valid = math.isfinite(bearing) and 0.0 <= bearing < 360.0
speed = speed if has_fix and speed_valid else 0.0
bearing = bearing if has_fix and bearing_valid else 0.0
height = float(altitude["GNSS_Ortung_Hoehe"])
height_valid = math.isfinite(height) and -500.0 <= height <= 7686.0
heading_rad = math.radians(bearing)
return {
"latitude": latitude,
"longitude": longitude,
"altitude": height if has_fix and height_valid else 0.0,
"speed": speed,
"bearingDeg": bearing,
"horizontalAccuracy": 20.0,
"unixTimestampMillis": timestamp_ms,
"verticalAccuracy": 50.0 if height_valid else 500.0,
"bearingAccuracyDeg": 10.0 if has_fix and bearing_valid and speed > 1.0 else 180.0,
"speedAccuracy": 1.5 if speed_valid and bearing_valid else 100.0,
"hasFix": has_fix,
"satelliteCount": satellite_count if 0 <= satellite_count <= 31 else 0,
"vNED": [speed * math.cos(heading_rad), speed * math.sin(heading_rad), 0.0] if bearing_valid else [0.0, 0.0, 0.0],
}
FORD_MACH_E_GPS_MESSAGES = (
"APIMGPS_Data_Nav_1_FD1",
"APIMGPS_Data_Nav_2_FD1",
"APIMGPS_Data_Nav_3_FD1",
)
CHEVROLET_BOLT_GPS_MESSAGES = ("TCICOnStarGPSPosition",)
VOLKSWAGEN_TAOS_GPS_MESSAGES = ("GNSS_01", "GNSS_02", "GNSS_04", "GNSS_05")
CHEVROLET_BOLT_GPS_CARS = (
GM_CAR.CHEVROLET_BOLT_ACC_2022_2023,
@@ -141,6 +196,11 @@ CHEVROLET_BOLT_GPS_CARS = (
CAR_GPS_CONFIGS: dict[str, CarGpsConfig] = {
VOLKSWAGEN_CAR.VOLKSWAGEN_TAOS_MK1: CarGpsConfig(
brand="volkswagen",
messages=VOLKSWAGEN_TAOS_GPS_MESSAGES,
decoder=parse_volkswagen_taos_can_gps,
),
FORD_CAR.FORD_MUSTANG_MACH_E_MK1: CarGpsConfig(
brand="ford",
messages=FORD_MACH_E_GPS_MESSAGES,
@@ -1065,21 +1065,27 @@ FW_VERSIONS = {
CAR.HONDA_CITY_7G: {
(Ecu.eps, 0x18da30f1, None): [
b'39990-T14-B030\x00\x00',
b'39990-T14-B510\x00\x00',
],
(Ecu.gateway, 0x18daeff1, None): [
b'38897-T14-M110\x00\x00',
b'38897-T14-M210\x00\x00',
],
(Ecu.srs, 0x18da53f1, None): [
b'77959-T00-B830\x00\x00',
b'77959-T14-B810\x00\x00',
],
(Ecu.fwdRadar, 0x18dab0f1, None): [
b'36161-T14-P050\x00\x00',
b'8S102-T14-P020\x00\x00',
],
(Ecu.vsa, 0x18da28f1, None): [
b'57114-T14-B030\x00\x00',
b'57114-T14-M510\x00\x00',
],
(Ecu.transmission, 0x18da1ef1, None): [
b'28101-63B-M420\x00\x00',
b'28101-63B-M510\x00\x00',
],
},
CAR.HONDA_PASSPORT_4G: {
+1 -1
View File
@@ -277,7 +277,7 @@ class CAR(Platforms):
flags=HondaFlags.BOSCH_RADARLESS,
)
HONDA_CITY_7G = HondaBoschPlatformConfig(
[HondaCarDocs("Honda City (Brazil only) 2023", "All")],
[HondaCarDocs("Honda City (Brazil only) 2023-25", "All")],
CarSpecs(mass=3125 * CV.LB_TO_KG, wheelbase=2.6, steerRatio=19.0, centerToFrontRatio=0.41, minSteerSpeed=23. * CV.KPH_TO_MS),
{Bus.pt: 'honda_bosch_radarless_generated'},
flags=HondaFlags.BOSCH_RADARLESS,
@@ -42,9 +42,11 @@ IONIQ_6_RESPONSE_MULTIPLIER = 1.2
IONIQ_6_CANFD_SCC_ACCEL_STEP = (6.0 / 50.0) * IONIQ_6_RESPONSE_MULTIPLIER
IONIQ_6_CANFD_SCC_DECEL_STEP = (15.0 / 50.0) * IONIQ_6_RESPONSE_MULTIPLIER
EV9_CANFD_SCC_DECEL_STEP = 10.0 / 50.0
RAY_PEDAL_COMMAND_CAP = 0.35 # Ray firmware voltage scaling is route-derived; validate before raising.
RAY_PEDAL_RATE_UP = 0.012 # per 25 Hz command (0.30 normalized pedal per second)
RAY_PEDAL_COMMAND_CAP = 0.55
RAY_PEDAL_RATE_UP = 0.02
RAY_PEDAL_RATE_DOWN = 0.06
RAY_PEDAL_OVERSPEED_CUTOFF = 0.5
RAY_PEDAL_TAPER_BELOW_TARGET = 0.75
GENESIS_G90_STOP_HOLD_SPEED_BP = [0.0, 0.03, 0.08, 0.16, 0.3, 0.5, 0.8, 1.2, 2.0, 3.0]
GENESIS_G90_STOP_HOLD_ACCEL_V = [-0.10, -0.10, -0.12, -0.18, -0.30, -0.50, -0.75, -1.00, -1.40, -1.80]
GENESIS_G90_STOP_HOLD_RELAX_SPEED_BP = [0.0, 0.08, 0.16, 0.3, 0.5, 0.8, 1.2, 2.0, 3.0]
@@ -441,12 +443,6 @@ def suppress_redundant_gv70_brake_cancel(CP, brake_pressed: bool, lat_active: bo
)
def clear_ioniq_6_torque_when_request_inactive(CP, apply_torque: int, apply_steer_req: bool) -> int:
if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and not apply_steer_req:
return 0
return apply_torque
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
@@ -642,8 +638,6 @@ class CarController(CarControllerBase):
if not CC.latActive:
apply_torque = 0
apply_torque = clear_ioniq_6_torque_when_request_inactive(self.CP, apply_torque, apply_steer_req)
# Hold torque with induced temporary fault when cutting the actuation bit
# FIXME: we don't use this with CAN FD?
torque_fault = CC.latActive and not apply_steer_req
@@ -776,7 +770,8 @@ class CarController(CarControllerBase):
lead_data = self._can_lead_data.update(lead_distance, lead_rel_speed, lead_visible)
# HUD messages
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint,
stinger_hud_enabled = CC.enabled or (self.CP.carFingerprint == CAR.KIA_STINGER_2022 and CC.latActive)
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(stinger_hud_enabled, self.car_fingerprint,
hud_control)
if blended_hda2:
@@ -817,12 +812,12 @@ class CarController(CarControllerBase):
# Button messages
if not self.long_active_ecu:
if self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
elif self._ray_pedal and CC.longActive and CS.out.cruiseState.enabled:
if (self.frame - self.last_button_frame) * DT_CTRL > 0.1:
if self._ray_pedal and CC.enabled and CS.out.cruiseState.enabled:
if (self.frame - self.last_button_frame) * DT_CTRL > 0.04:
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
self.last_button_frame = self.frame
elif self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
elif CC.cruiseControl.resume and not self._ray_pedal:
# send resume at a max freq of 10Hz
if (self.frame - self.last_button_frame) * DT_CTRL > 0.1:
@@ -837,14 +832,29 @@ class CarController(CarControllerBase):
if self._ray_pedal and self.frame % 4 == 0:
pedal_ready = CS.ray_pedal_valid and CS.ray_pedal_state == 0
pedal_active = (CC.longActive and pedal_ready and not CC.cruiseControl.override and
not CS.out.gasPressed and not CS.out.brakePressed and
not CS.out.cruiseState.enabled and CS.out.vEgo >= self.CP.minEnableSpeed)
not CS.out.gasPressed and not CS.out.brakePressed)
if pedal_active:
target = float(np.clip(accel / CarControllerParams.ACCEL_MAX * RAY_PEDAL_COMMAND_CAP,
0.0, RAY_PEDAL_COMMAND_CAP))
self._ray_pedal_gas_last = rate_limit(
target, self._ray_pedal_gas_last, -RAY_PEDAL_RATE_DOWN, RAY_PEDAL_RATE_UP,
)
set_speed = hud_control.setSpeed
if not np.isfinite(set_speed) or set_speed < 1.0:
self._ray_pedal_gas_last = 0.0
else:
speed_error = set_speed - CS.out.vEgo
if speed_error <= -RAY_PEDAL_OVERSPEED_CUTOFF:
self._ray_pedal_gas_last = 0.0
else:
pedal_offset = float(np.interp(CS.out.vEgo, [0., 2., 4., 8., 12., 20.],
[0.08, 0.13, 0.20, 0.32, 0.42, 0.48]))
pedal_gain = 2.0 if accel < 0.0 else 0.22
target = float(np.clip(pedal_offset + accel * pedal_gain, 0.0, RAY_PEDAL_COMMAND_CAP))
if speed_error < 0.0:
target *= float(np.clip(0.65 * (1.0 + speed_error / RAY_PEDAL_OVERSPEED_CUTOFF), 0.0, 1.0))
elif speed_error < RAY_PEDAL_TAPER_BELOW_TARGET:
target *= 0.65 + 0.35 * speed_error / RAY_PEDAL_TAPER_BELOW_TARGET
if target <= 0.001:
self._ray_pedal_gas_last = 0.0
else:
next_gas = rate_limit(target, self._ray_pedal_gas_last, -RAY_PEDAL_RATE_DOWN, RAY_PEDAL_RATE_UP)
self._ray_pedal_gas_last = min(next_gas, target)
else:
self._ray_pedal_gas_last = 0.0
can_sends.append(create_gas_interceptor_command(
@@ -1033,9 +1043,12 @@ class CarController(CarControllerBase):
left_sound_active=left_warning.sound_active, right_sound_active=right_warning.sound_active,
)
else:
host_speed = getattr(CS.out, "vEgoRaw", None) \
if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else None
adrv_messages = hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame,
car_fingerprint=self.CP.carFingerprint,
drive_gear=drive_gear)
drive_gear=drive_gear,
v_ego=host_speed)
can_sends.extend(adrv_messages)
# The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat
# and stops publishing object tracks when it disappears.
@@ -26,6 +26,33 @@ ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL)
BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: ButtonType.decelCruise,
Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel}
class Ev6AolArmingState:
def __init__(self, lkas_on_engage: bool):
self.lkas_on_engage = lkas_on_engage
self.main_on = False
self.lkas_on = False
self.prev_main_button = False
self.prev_lkas_button = False
self.prev_cruise_button = Buttons.NONE
def update(self, main_button: bool, lkas_button: bool, cruise_button: int):
if main_button and not self.prev_main_button:
self.main_on = not self.main_on
if lkas_button and not self.prev_lkas_button:
self.lkas_on = not self.lkas_on
if (self.lkas_on_engage and cruise_button != self.prev_cruise_button and
self.prev_cruise_button in (Buttons.SET_DECEL, Buttons.RES_ACCEL)):
self.lkas_on = True
self.prev_main_button = main_button
self.prev_lkas_button = lkas_button
self.prev_cruise_button = cruise_button
@property
def authorized(self) -> bool:
return self.main_on or self.lkas_on
IONIQ_6_BLINDSPOT_RIGHT_MASK = 0x08
IONIQ_6_BLINDSPOT_LEFT_MASK = 0x10
CANFD_CAMERA_LEAD_MIN_DISTANCE = 0.1
@@ -138,6 +165,13 @@ class CarState(CarStateBase):
self.buttons_counter = 0
self.main_cruise_on = False
self.main_cruise_tracking = bool(getattr(FPCP, "flags", 0) & HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING)
self.ev6_aol_arming = None
if CP.carFingerprint == CAR.KIA_EV6 and CP.openpilotLongitudinalControl and not CP.pcmCruise:
lkas_on_engage = any(
config.safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE
for config in (*CP.safetyConfigs, *FPCP.safetyConfigs)
)
self.ev6_aol_arming = Ev6AolArmingState(lkas_on_engage)
if CP.carFingerprint == CAR.KIA_RAY_EV:
self.ray_pedal_state = 5
self.ray_pedal_valid = False
@@ -181,6 +215,10 @@ class CarState(CarStateBase):
# Main button also can trigger an engagement on these cars
return any(btn in ENABLE_BUTTONS for btn in self.cruise_buttons) or any(self.main_buttons)
@property
def ev6_aol_authorized(self) -> bool:
return self.ev6_aol_arming is not None and self.ev6_aol_arming.authorized
def update_main_cruise(self, ret: structs.CarState,
button_events: list[structs.CarState.ButtonEvent] | None = None) -> bool:
button_events = ret.buttonEvents if button_events is None else button_events
@@ -640,6 +678,13 @@ class CarState(CarStateBase):
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}),
*create_button_events(self.left_paddle, prev_left_paddle, {1: ButtonType.altButton2})]
if self.ev6_aol_arming is not None:
buttons = cp.vl_all[self.cruise_btns_msg_canfd]
for main, lkas, cruise in zip(buttons["ADAPTIVE_CRUISE_MAIN_BTN"], buttons["LDA_BTN"], buttons["CRUISE_BUTTONS"], strict=True):
self.ev6_aol_arming.update(bool(main), bool(lkas), int(cruise))
if not ret.cruiseState.available:
self.ev6_aol_arming.main_on = False
if self.CP.openpilotLongitudinalControl and (self.CP.carFingerprint == CAR.KIA_EV9 or self.main_cruise_tracking):
ret.cruiseState.available = self.update_main_cruise(ret)
@@ -1335,6 +1335,7 @@ FW_VERSIONS = {
(Ecu.fwdCamera, 0x7c4, None): [
b'\xf1\x00CL4 MFC AT CAN LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.04 99210-GG100 251205',
],
(Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG000 ',
@@ -1556,6 +1557,7 @@ FW_VERSIONS = {
},
CAR.HYUNDAI_STARIA_4TH_GEN: {
(Ecu.fwdCamera, 0x7c4, None): [
b'\xf1\x00US4 MFC AT AUS RHD 1.00 1.04 99211-CG000 210819',
b'\xf1\x00US4 MFC AT KOR LHD 1.00 1.06 99211-CG000 230524',
],
(Ecu.fwdRadar, 0x7d0, None): [
@@ -12,7 +12,7 @@ _adrv_0x51_templates: dict[CAR, bytes] = {}
def cache_adrv_0x51_template(car_fingerprint: CAR, dat: bytes | None) -> None:
if car_fingerprint != CAR.KIA_EV6:
if car_fingerprint not in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN):
return
if dat is None:
@@ -21,15 +21,18 @@ def cache_adrv_0x51_template(car_fingerprint: CAR, dat: bytes | None) -> None:
_adrv_0x51_templates[car_fingerprint] = bytes(dat)
def create_adrv_0x51(packer, CAN, frame: int, car_fingerprint: CAR | None = None, drive_gear: bool = False):
def create_adrv_0x51(packer, CAN, frame: int, car_fingerprint: CAR | None = None, drive_gear: bool = False,
v_ego: float | None = None):
template = _adrv_0x51_templates.get(car_fingerprint)
if template is None:
return packer.make_can_msg("ADRV_0x51", CAN.ACAN, {})
# EV6 MRR30 tracks stop when the ADAS takeover replaces this platform payload with zeros.
dat = bytearray(template)
dat[2] = (template[2] + frame + 1) & 0xFF
dat[3] = (dat[3] & ~0x1) | int(drive_gear)
if car_fingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and v_ego is not None and np.isfinite(v_ego):
speed_raw = int(np.clip(round(v_ego * 100.0), 0, 65534))
dat[8:10] = speed_raw.to_bytes(2, "little")
crc = hkg_can_fd_checksum(0x51, None, dat)
dat[0] = crc & 0xFF
dat[1] = (crc >> 8) & 0xFF
@@ -816,13 +819,14 @@ def create_fca_warning_light(packer, CAN, frame):
return ret
def create_adrv_messages(packer, CAN, frame, blended_hda2=False, car_fingerprint=None, drive_gear=False):
def create_adrv_messages(packer, CAN, frame, blended_hda2=False, car_fingerprint=None, drive_gear=False,
v_ego=None):
# messages needed to car happy after disabling
# the ADAS Driving ECU to do longitudinal control
ret = []
ret.append(create_adrv_0x51(packer, CAN, frame, car_fingerprint, drive_gear))
ret.append(create_adrv_0x51(packer, CAN, frame, car_fingerprint, drive_gear, v_ego))
if blended_hda2:
return ret
@@ -201,6 +201,8 @@ class CarInterface(CarInterfaceBase):
if ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING:
ret.steerControlType = structs.CarParams.SteerControlType.angle
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CANFD_ANGLE_STEERING.value
if candidate == CAR.KIA_SPORTAGE_HEV_2026:
ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.CANFD_NO_STOCK_LKA.value
if candidate == CAR.HYUNDAI_IONIQ_6:
# Keep lateral active through stops: zeroing torque at standstill dropped the
# stop-turn hold and forced a rate-limit re-ramp from zero on every pull-away
@@ -313,7 +315,7 @@ class CarInterface(CarInterfaceBase):
ret.pcmCruise = False
ret.radarUnavailable = True
ret.autoResumeSng = False
ret.minEnableSpeed = 5.0 # pedal-only: no commanded friction brake/standstill hold
ret.minEnableSpeed = -1.0
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value
# Car specific configuration overrides
@@ -331,8 +333,8 @@ class CarInterface(CarInterfaceBase):
if candidate == CAR.HYUNDAI_ELANTRA_2021:
ret.longitudinalActuatorDelay = 0.22
ret.stopAccel = -0.85
ret.stoppingDecelRate = 0.35
ret.stopAccel = -1.1
ret.stoppingDecelRate = 0.55
if candidate == CAR.HYUNDAI_ELANTRA_HEV_2024:
ret.longitudinalActuatorDelay = 0.22
@@ -393,7 +395,7 @@ class CarInterface(CarInterfaceBase):
if not skip_disable_ecu:
disable_can_recv = can_recv
if CP.carFingerprint == CAR.KIA_EV6 and can_recv is not None:
if CP.carFingerprint in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and can_recv is not None:
hyundaicanfd.cache_adrv_0x51_template(CP.carFingerprint, None)
base_can_recv = can_recv
adrv_bus = CanBus(CP).ACAN
@@ -20,9 +20,8 @@ from opendbc.car.hyundai.carcontroller import CarController, CANCEL_BUTTON_DELAY
should_track_stop_accel_directly_for_car, \
preserve_stock_canfd_lfa_status, \
preserve_stock_canfd_lkas_status, \
suppress_redundant_gv70_brake_cancel, \
clear_ioniq_6_torque_when_request_inactive
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
suppress_redundant_gv70_brake_cancel
from opendbc.car.hyundai.carstate import CarState, Ev6AolArmingState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
get_canfd_cruise_available
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX, get_communication_control_request
from opendbc.car.hyundai import hyundaican, hyundaicanfd
@@ -131,6 +130,73 @@ def get_test_toggles() -> SimpleNamespace:
class TestHyundaiFingerprint:
@pytest.mark.parametrize("candidate, alpha_long, needs_arming", (
(CAR.KIA_EV6, True, True), (CAR.KIA_EV6, False, False),
(CAR.KIA_EV6_2025, True, False), (CAR.KIA_EV9, True, False),
(CAR.HYUNDAI_IONIQ_5, True, False), (CAR.HYUNDAI_IONIQ_6, True, False),
))
def test_ev6_aol_arming_is_vehicle_specific(self, candidate, alpha_long, needs_arming):
toggles = get_test_toggles()
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], alpha_long, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(candidate, gen_empty_fingerprint(), [], CP, toggles)
CS = CarState(CP, FPCP)
assert (CS.ev6_aol_arming is not None) == needs_arming
assert not CS.ev6_aol_authorized
@pytest.mark.parametrize("alt_buttons", (False, True))
@pytest.mark.parametrize("lkas_on_engage", (False, True))
def test_ev6_aol_uses_all_physical_button_samples(self, alt_buttons, lkas_on_engage):
toggles = get_test_toggles()
toggles.always_on_lateral_lkas = lkas_on_engage
fingerprint = gen_empty_fingerprint()
if not alt_buttons:
fingerprint[0][0x1CF] = 8
CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], True, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_EV6, fingerprint, [], CP, toggles)
CS = CarState(CP, FPCP)
parsers = CS.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
buttons_msg = CS.cruise_btns_msg_canfd
frame = 0
def update(samples, acc_available=True):
nonlocal frame
frame += 1
msgs = [packer.make_can_msg(buttons_msg, CanBus(CP).ECAN, {
"ADAPTIVE_CRUISE_MAIN_BTN": main, "LDA_BTN": lkas, "CRUISE_BUTTONS": cruise,
}) for main, lkas, cruise in samples]
msgs.append(packer.make_can_msg("TCS", CanBus(CP).ECAN, {"ACCEnable": 0 if acc_available else 1}))
parsers[Bus.pt].update([(frame * 10_000_000, msgs)])
CS.update(parsers, toggles)
return CS.ev6_aol_authorized
assert not update([(0, 0, Buttons.NONE)])
assert update([(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
assert update([])
assert not update([(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
assert update([(1, 0, Buttons.NONE), (1, 0, Buttons.NONE)])
assert update([(0, 0, Buttons.NONE)])
assert not update([(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
assert update([(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
assert not update([], acc_available=False)
assert not update([])
assert not update([(0, 0, Buttons.SET_DECEL)])
assert update([(0, 0, Buttons.NONE)]) == lkas_on_engage
assert update([]) == lkas_on_engage
@pytest.mark.parametrize("button", (Buttons.SET_DECEL, Buttons.RES_ACCEL))
@pytest.mark.parametrize("lkas_on_engage", (False, True))
def test_ev6_aol_engagement_latch_matches_safety_flag(self, button, lkas_on_engage):
state = Ev6AolArmingState(lkas_on_engage)
state.update(False, False, button)
assert not state.authorized
state.update(False, False, Buttons.NONE)
assert state.authorized == lkas_on_engage
state.update(False, False, Buttons.CANCEL)
assert state.authorized == lkas_on_engage
state.update(False, True, Buttons.NONE)
assert state.authorized != lkas_on_engage
def test_egmp_communication_control_paths(self):
stock_request = bytes([0x28, 0x83, 0x01])
radar_keepalive_request = bytes([0x28, 0x01, 0x01])
@@ -553,6 +619,13 @@ class TestHyundaiFingerprint:
CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, None)
assert CP.flags & HyundaiFlags.SEND_LFA
@pytest.mark.parametrize("candidate", list(CAR))
def test_no_stock_lka_safety_flag_is_sportage_only(self, candidate):
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False, None)
if CP.flags & HyundaiFlags.CANFD:
assert bool(CP.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.CANFD_NO_STOCK_LKA) == \
(candidate == CAR.KIA_SPORTAGE_HEV_2026)
def test_smart_mdps_allows_low_speed_steering(self):
candidate = CAR.HYUNDAI_IONIQ_EV_LTD
@@ -680,14 +753,7 @@ class TestHyundaiFingerprint:
assert not (CP.flags & HyundaiFlags.CANFD_LKA_STEERING)
assert bool(CP.flags & HyundaiFlags.CANFD_CAMERA_SCC)
def test_ioniq_6_clears_torque_with_inactive_safety_request(self):
ioniq_6_cp = SimpleNamespace(carFingerprint=CAR.HYUNDAI_IONIQ_6)
other_cp = SimpleNamespace(carFingerprint=CAR.KIA_EV6)
assert clear_ioniq_6_torque_when_request_inactive(ioniq_6_cp, -409, False) == 0
assert clear_ioniq_6_torque_when_request_inactive(ioniq_6_cp, -409, True) == -409
assert clear_ioniq_6_torque_when_request_inactive(other_cp, -409, False) == -409
def test_palisade_2023_uses_can_canfd_blended_layout(self):
palisade_2023 = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, gen_empty_fingerprint(), [], True, False, False, None)
assert palisade_2023.flags & HyundaiFlags.CAN_CANFD_BLENDED
assert DBC[palisade_2023.carFingerprint][Bus.pt] == "hyundai_palisade_2023_generated"
@@ -1087,6 +1153,31 @@ class TestHyundaiFingerprint:
assert not (FPCP.flags & HyundaiStarPilotFlags.HAS_LKAS12)
@pytest.mark.parametrize("length, expected", ((6, True), (8, False)))
def test_stinger_only_replaces_six_byte_lkas12(self, length, expected):
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x53E] = length
CP = CarInterface.get_params(CAR.KIA_STINGER_2022, fingerprint, [], True, False, False, None)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_STINGER_2022, fingerprint, [], CP, get_test_toggles())
assert bool(FPCP.flags & HyundaiStarPilotFlags.HAS_LKAS12) is expected
@pytest.mark.parametrize("alpha_long, main_aol, expected", (
(True, True, True), (True, False, True), (False, True, False),
))
def test_stinger_aol_latches_lkas_after_long_engagement(self, alpha_long, main_aol, expected):
toggles = get_test_toggles()
toggles.always_on_lateral_main = main_aol
fingerprint = gen_empty_fingerprint()
CP = CarInterface.get_params(CAR.KIA_STINGER_2022, fingerprint, [], alpha_long, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_STINGER_2022, fingerprint, [], CP, toggles)
assert bool(FPCP.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE) is expected
sonata_cp = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], alpha_long, False, False, toggles)
sonata_fpcp = CarInterface.get_starpilot_params(CAR.HYUNDAI_SONATA, fingerprint, [], sonata_cp, toggles)
assert not (sonata_fpcp.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE)
def test_ray_ev_does_not_treat_eight_byte_485_as_lfa(self):
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x485] = 8
@@ -1292,6 +1383,103 @@ class TestHyundaiFingerprint:
assert canfd_alt_buttons_cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS
assert not canfd_alt_buttons_fpcp.redneckCruiseAvailable
def test_sportage_hev_hda2_redneck_uses_stock_scc(self, monkeypatch):
class FakeParams:
def __init__(self, *args, **kwargs):
pass
@staticmethod
def get_bool(key):
return key == "RedneckCruise"
monkeypatch.setattr("opendbc.car.interfaces.Params", FakeParams)
toggles = get_test_toggles()
fingerprint = gen_empty_fingerprint()
can_bus = CanBus(None, fingerprint, True)
fingerprint[can_bus.CAM][0x110] = 32
fingerprint[can_bus.ECAN][0x1CF] = 8
CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], CP, toggles)
assert CP.flags & HyundaiFlags.CANFD_LKA_STEERING
assert FPCP.redneckCruiseAvailable
assert not FPCP.pcmCruiseSpeed
assert CP.pcmCruise
assert not CP.openpilotLongitudinalControl
assert not CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG
controller = CarInterface(CP, FPCP).CC
assert not controller.long_active_ecu
controller.frame = 30
CS = SimpleNamespace(redneck_send_button=1, buttons_counter=5)
msgs = controller._create_canfd_redneck_button_messages(CS)
assert len(msgs) == 20
assert all(msg[0] == 0x1CF and msg[2] == can_bus.ECAN for msg in msgs)
assert all(msg[1][2] & 0x7 == Buttons.RES_ACCEL for msg in msgs)
controller.frame = 60
CS.redneck_send_button = 2
msgs = controller._create_canfd_redneck_button_messages(CS)
assert len(msgs) == 20
assert all(msg[1][2] & 0x7 == Buttons.SET_DECEL for msg in msgs)
monkeypatch.setattr(FakeParams, "get_bool", staticmethod(lambda key: False))
CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], CP, toggles)
assert FPCP.redneckCruiseAvailable
assert FPCP.pcmCruiseSpeed
assert CP.pcmCruise
assert not CP.openpilotLongitudinalControl
def test_sportage_redneck_rejects_unverified_button_layouts(self, monkeypatch):
class FakeParams:
def __init__(self, *args, **kwargs):
pass
@staticmethod
def get_bool(key):
return key == "RedneckCruise"
monkeypatch.setattr("opendbc.car.interfaces.Params", FakeParams)
toggles = get_test_toggles()
for button_address, button_bus, button_length, lka_steering in (
(0x1AA, 1, 16, True),
(0x1CF, 0, 8, True),
(0x1CF, 0, 8, False),
(0x1CF, 1, 16, True),
):
fingerprint = gen_empty_fingerprint()
can_bus = CanBus(None, fingerprint, lka_steering)
if lka_steering:
fingerprint[can_bus.CAM][0x110] = 32
fingerprint[button_bus][button_address] = button_length
CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], CP, toggles)
assert not FPCP.redneckCruiseAvailable
assert FPCP.pcmCruiseSpeed
assert CP.pcmCruise
assert not CP.openpilotLongitudinalControl
fingerprint = gen_empty_fingerprint()
can_bus = CanBus(None, fingerprint, True)
fingerprint[can_bus.CAM][0x110] = 32
fingerprint[can_bus.ECAN][0x1CF] = 8
fingerprint[can_bus.ECAN][0x1AA] = 16
CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], CP, toggles)
assert not FPCP.redneckCruiseAvailable
fingerprint = gen_empty_fingerprint()
can_bus = CanBus(None, fingerprint, True)
fingerprint[can_bus.CAM][0x110] = 32
fingerprint[can_bus.ECAN][0x1CF] = 8
CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, toggles)
CP.openpilotLongitudinalControl = True
FPCP = CarInterface.get_starpilot_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], CP, toggles)
assert not FPCP.redneckCruiseAvailable
def test_hyundai_non_scc_without_redneck_keeps_stock_longitudinal_mode(self, monkeypatch):
class FakeParams:
def __init__(self, *args, **kwargs):
@@ -1631,8 +1819,8 @@ class TestHyundaiFingerprint:
CP = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_2021, gen_empty_fingerprint(), [], True, False, False, toggles)
assert CP.longitudinalActuatorDelay == pytest.approx(0.22)
assert CP.stopAccel == pytest.approx(-0.85)
assert CP.stoppingDecelRate == pytest.approx(0.35)
assert CP.stopAccel == pytest.approx(-1.1)
assert CP.stoppingDecelRate == pytest.approx(0.55)
def test_elantra_hev_2024_longitudinal_delay_matches_observed_response(self):
toggles = get_test_toggles()
@@ -1675,6 +1863,69 @@ class TestHyundaiFingerprint:
assert exact
assert matches == {candidate}
@pytest.mark.parametrize("alpha_long", (False, True))
@pytest.mark.parametrize("alt_buttons", (False, True))
@pytest.mark.parametrize("signal, value, button_type", (
("LDA_BTN", 1, ButtonType.lkas),
("ADAPTIVE_CRUISE_MAIN_BTN", 1, ButtonType.mainCruise),
("CRUISE_BUTTONS", Buttons.RES_ACCEL, ButtonType.accelCruise),
("CRUISE_BUTTONS", Buttons.SET_DECEL, ButtonType.decelCruise),
("CRUISE_BUTTONS", Buttons.CANCEL, ButtonType.cancel),
))
def test_k4_2025_2026_physical_button_events(self, alpha_long, alt_buttons, signal, value, button_type):
toggles = get_test_toggles()
fingerprint = gen_empty_fingerprint()
if not alt_buttons:
fingerprint[0][0x1CF] = 8
CP = CarInterface.get_params(CAR.KIA_K4_2025, fingerprint, [], alpha_long, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_K4_2025, fingerprint, [], CP, toggles)
car_state = CarState(CP, FPCP)
parsers = car_state.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
def update(button_value, frame):
msg = packer.make_can_msg(car_state.cruise_btns_msg_canfd, CanBus(CP).ECAN, {signal: button_value})
parsers[Bus.pt].update([(frame * 10_000_000, [msg])])
return car_state.update(parsers, toggles)[0].buttonEvents
assert not update(0, 1)
pressed = update(value, 2)
assert [(event.type, event.pressed) for event in pressed] == [(button_type, True)]
released = update(0, 3)
assert [(event.type, event.pressed) for event in released] == [(button_type, False)]
@pytest.mark.parametrize("camera_fw", [
b'\xf1\x00CL4 MFC AT CAN LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.04 99210-GG100 251205',
])
@pytest.mark.parametrize("radar_fw", [
b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG000 ',
b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG100 ',
])
def test_k4_2025_2026_fw_exact_matches(self, camera_fw, radar_fw):
car_fw = [
CarParams.CarFw(ecu=Ecu.fwdCamera, fwVersion=camera_fw, address=0x7c4, brand="hyundai"),
CarParams.CarFw(ecu=Ecu.fwdRadar, fwVersion=radar_fw, address=0x7d0, brand="hyundai"),
]
exact, matches = match_fw_to_car(car_fw, "", allow_exact=True, allow_fuzzy=False, log=False)
assert exact
assert matches == {CAR.KIA_K4_2025}
def test_staria_2023_australian_route_fw_exact_matches(self):
route_fw = {
(Ecu.fwdCamera, 0x7c4): b'\xf1\x00US4 MFC AT AUS RHD 1.00 1.04 99211-CG000 210819',
(Ecu.fwdRadar, 0x7d0): b'\xf1\x00US4_ RDR ----- 1.00 1.00 99110-CG000 ',
}
car_fw = [
CarParams.CarFw(ecu=ecu, fwVersion=version, address=address, subAddress=0, brand="hyundai")
for (ecu, address), version in route_fw.items()
]
exact, matches = match_fw_to_car(car_fw, "KMFYFX71MPU095311", allow_fuzzy=False, log=False)
assert exact
assert matches == {CAR.HYUNDAI_STARIA_4TH_GEN}
def test_kona_ev_non_scc_has_no_dedicated_fw_coverage(self):
assert CAR.HYUNDAI_KONA_EV_NON_SCC not in FW_VERSIONS
@@ -32,7 +32,7 @@ def test_ray_pedal_fingerprint_isolation(candidate, fingerprint, has_pedal):
if has_pedal:
assert not CP.pcmCruise
assert CP.safetyConfigs[-1].safetyParam == 0x9405
assert CP.minEnableSpeed == 5.0
assert CP.minEnableSpeed == -1.0
assert not CP.autoResumeSng
FPCP = CarInterface.get_starpilot_params(candidate, fingerprint, [], CP, SimpleNamespace())
assert FPCP.canUsePedal
@@ -128,13 +128,14 @@ def test_ray_without_pedal_keeps_native_gas_detection():
assert ret.gasPressed
def test_ray_controller_heartbeats_and_only_actuates_when_ready():
@pytest.mark.parametrize("speed", [0.0, 0.1, 1.0, 4.9, 5.0, 12.0])
def test_ray_controller_heartbeats_and_only_actuates_when_ready(speed):
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
controller = CarController(DBC[CP.carFingerprint], CP)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0), ("CLU11", 0)], 0)
CS = SimpleNamespace(
lkas11=parser.vl["LKAS11"], clu11=parser.vl["CLU11"],
out=SimpleNamespace(vEgo=12.0, gasPressed=False, brakePressed=False,
out=SimpleNamespace(vEgo=speed, gasPressed=False, brakePressed=False,
cruiseState=SimpleNamespace(enabled=False)),
ray_pedal_valid=True, ray_pedal_state=5, is_metric=True,
)
@@ -144,6 +145,7 @@ def test_ray_controller_heartbeats_and_only_actuates_when_ready():
)
hud = SimpleNamespace(
visualAlert=CarControl.HUDControl.VisualAlert.none,
setSpeed=20.0,
leftLaneVisible=True, rightLaneVisible=True,
leftLaneDepart=False, rightLaneDepart=False,
)
@@ -166,5 +168,124 @@ def test_ray_controller_heartbeats_and_only_actuates_when_ready():
controller.frame = 16
messages = controller.create_can_msgs(True, 0, False, 0.0, 2.0, False,
hud, actuators, CS, CC, 2, 0)
assert next(dat for addr, dat, bus in messages if addr == 0x200 and bus == 0)[:4] == bytes(4)
assert any(addr == 0x4F1 and bus == 0 for addr, _, bus in messages) # cancel stock CC
assert next(dat for addr, dat, bus in messages if addr == 0x200 and bus == 0)[4] & 0x80
assert any(addr == 0x4F1 and bus == 0 and dat[0] & 7 == 4 for addr, dat, bus in messages)
CS.out.cruiseState.enabled = False
CS.out.brakePressed = True
assert pedal_msg(2.0, 20)[:4] == bytes(4)
CS.out.brakePressed = False
assert pedal_msg(2.0, 24)[4] & 0x80
assert controller._ray_pedal_gas_last == pytest.approx(0.02)
assert pedal_msg(2.0, 28)[4] & 0x80
assert controller._ray_pedal_gas_last == pytest.approx(0.04)
CS.out.brakePressed = True
assert pedal_msg(2.0, 32)[:4] == bytes(4)
assert controller._ray_pedal_gas_last == 0.0
CS.out.brakePressed = False
assert pedal_msg(2.0, 36)[4] & 0x80
assert controller._ray_pedal_gas_last == pytest.approx(0.02)
CC.longActive = False
assert pedal_msg(2.0, 40)[:4] == bytes(4)
CC.longActive = True
CC.cruiseControl.override = True
assert pedal_msg(2.0, 44)[:4] == bytes(4)
CC.cruiseControl.override = False
CS.ray_pedal_valid = False
assert pedal_msg(2.0, 48)[:4] == bytes(4)
CS.ray_pedal_valid = True
for fault in range(1, 6):
CS.ray_pedal_state = fault
assert pedal_msg(2.0, 48 + 4 * fault)[:4] == bytes(4)
CS.ray_pedal_state = 0
CS.out.vEgo = 10.0
assert pedal_msg(0.0, 72)[4] & 0x80
assert pedal_msg(-1.0, 76)[:4] == bytes(4)
CS.out.vEgo = 12.0
for frame in range(80, 80 + 4 * 40, 4):
pedal_msg(1.5, frame)
assert controller._ray_pedal_gas_last == pytest.approx(0.55)
for frame in range(240, 240 + 4 * 12, 4):
dat = pedal_msg(-1.5, frame)
assert dat[:4] == bytes(4)
for frame in range(288, 288 + 4 * 40, 4):
pedal_msg(1.5, frame)
assert controller._ray_pedal_gas_last == pytest.approx(0.55)
hud.setSpeed = 12.0
pedal_msg(1.5, 448)
assert controller._ray_pedal_gas_last == pytest.approx(0.55 * 0.65)
hud.setSpeed = 11.8
dat = pedal_msg(1.5, 452)
assert controller._ray_pedal_gas_last == pytest.approx(0.55 * 0.65 * 0.6)
assert dat[4] & 0x80
hud.setSpeed = 11.4
assert pedal_msg(1.5, 456)[:4] == bytes(4)
hud.setSpeed = float('nan')
assert pedal_msg(1.5, 460)[:4] == bytes(4)
hud.setSpeed = 20.0
assert pedal_msg(-0.3, 464)[:4] == bytes(4)
CS.out.vEgo = 15.0
hud.setSpeed = 53.0 / 3.6
pedal_msg(-0.16, 468)
assert controller._ray_pedal_gas_last < 0.1
CS.out.vEgo = 12.0
hud.setSpeed = 8.0 / 3.6
assert pedal_msg(-0.3, 472)[:4] == bytes(4)
hud.setSpeed = 145.0 / 3.6
assert pedal_msg(1.5, 476)[4] & 0x80
assert controller._ray_pedal_gas_last == pytest.approx(0.02)
assert pedal_msg(1.5, 480)[4] & 0x80
assert controller._ray_pedal_gas_last == pytest.approx(0.04)
assert pedal_msg(-0.3, 484)[:4] == bytes(4)
@pytest.mark.parametrize("candidate", [CAR.KIA_RAY_EV, CAR.HYUNDAI_KONA_EV_NON_SCC])
def test_ray_stock_cruise_cancellation_survives_accelerator_override(candidate):
CP = CarInterface.get_params(candidate, ray_fingerprint(), [], False, False, False, None)
controller = CarController(DBC[CP.carFingerprint], CP)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0), ("CLU11", 0)], 0)
CS = SimpleNamespace(
lkas11=parser.vl["LKAS11"], clu11=parser.vl["CLU11"],
out=SimpleNamespace(vEgo=12.0, gasPressed=True, brakePressed=False,
cruiseState=SimpleNamespace(enabled=True)),
ray_pedal_valid=True, ray_pedal_state=0, is_metric=True,
)
CC = SimpleNamespace(
enabled=True, longActive=False, latActive=True,
cruiseControl=SimpleNamespace(cancel=False, resume=False, override=True),
)
hud = SimpleNamespace(
visualAlert=CarControl.HUDControl.VisualAlert.none,
setSpeed=20.0,
leftLaneVisible=True, rightLaneVisible=True, leftLaneDepart=False, rightLaneDepart=False,
)
actuators = SimpleNamespace(longControlState=CarControl.Actuators.LongControlState.off)
controller._create_can_redneck_button_messages = lambda _: []
def messages(frame):
controller.frame = frame
return controller.create_can_msgs(True, 0, False, 0.0, 2.0, False,
hud, actuators, CS, CC, 2, 0)
def cancel_frames(msgs):
return [dat for addr, dat, bus in msgs if addr == 0x4F1 and bus == 0 and dat[0] & 7 == 4]
msgs = messages(20)
assert bool(cancel_frames(msgs)) is (candidate == CAR.KIA_RAY_EV)
if candidate == CAR.KIA_RAY_EV:
pedal = next(dat for addr, dat, bus in msgs if addr == 0x200 and bus == 0)
assert pedal[:4] == bytes(4)
assert not (pedal[4] & 0x80)
assert not cancel_frames(messages(24)) # retain the existing cancellation rate limit
assert cancel_frames(messages(25))
assert cancel_frames(messages(32))
CS.out.cruiseState.enabled = False
assert not cancel_frames(messages(44))
CS.out.cruiseState.enabled = True
CC.enabled = False
assert not cancel_frames(messages(56)) # AOL alone must not cancel native cruise
@@ -117,6 +117,7 @@ class HyundaiSafetyFlags(IntFlag):
class HyundaiStarPilotSafetyFlags(IntFlag):
CANFD_NO_STOCK_LKA = 4096 # CAN-FD only; classic CAN uses this bit for NON_SCC.
AOL_MAIN_LKAS_ON_ENGAGE = 128
AOL_MAIN_LKAS_SYNC = 32
HAS_LDA_BUTTON = 1024
@@ -605,6 +606,7 @@ class CAR(Platforms):
KIA_K4_2025 = HyundaiCanFDPlatformConfig(
[
HyundaiCarDocs("Kia K4 (without HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_a])),
HyundaiCarDocs("Kia K4 (without HDA II) 2026", car_parts=CarParts.common([CarHarness.hyundai_a])),
HyundaiCarDocs("Kia K4 (with HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_r])),
],
CarSpecs(mass=2987 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=13.4),
+21 -5
View File
@@ -244,15 +244,28 @@ class CarInterfaceBase(ABC):
if 0x1FA in fingerprint[CAN.ECAN]:
fp_ret.flags |= HyundaiStarPilotFlags.SPEED_LIMIT_AVAILABLE.value
if candidate != HYUNDAI.KIA_RAY_EV and not (CP.flags & HyundaiFlags.CANFD) and 0x53E in fingerprint[2]:
if candidate != HYUNDAI.KIA_RAY_EV and not (CP.flags & HyundaiFlags.CANFD) and 0x53E in fingerprint[2] and \
(candidate != HYUNDAI.KIA_STINGER_2022 or fingerprint[2][0x53E] == 6):
fp_ret.flags |= HyundaiStarPilotFlags.HAS_LKAS12.value
fp_ret.redneckCruiseAvailable = (bool(CP.flags & HyundaiFlags.NON_SCC) and
not bool(CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS) and
not (candidate == HYUNDAI.KIA_RAY_EV and CP.enableGasInterceptorDEPRECATED))
sportage_stock_scc_buttons = (
candidate == HYUNDAI.KIA_SPORTAGE_HEV_2026 and
not CP.openpilotLongitudinalControl and
bool(CP.flags & HyundaiFlags.CANFD_LKA_STEERING) and
not bool(CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS) and
fingerprint[CAN.ECAN].get(0x1CF) == 8 and
0x1AA not in fingerprint[CAN.ECAN]
)
fp_ret.redneckCruiseAvailable = (
(bool(CP.flags & HyundaiFlags.NON_SCC) and
not bool(CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS) and
not (candidate == HYUNDAI.KIA_RAY_EV and CP.enableGasInterceptorDEPRECATED)) or
sportage_stock_scc_buttons
)
if fp_ret.redneckCruiseAvailable and params.get_bool("RedneckCruise"):
fp_ret.pcmCruiseSpeed = False
CP.openpilotLongitudinalControl = True
if CP.flags & HyundaiFlags.NON_SCC:
CP.openpilotLongitudinalControl = True
if candidate == HYUNDAI.HYUNDAI_ELANTRA_HEV_2024 and CP.openpilotLongitudinalControl:
fp_ret.flags |= HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING.value
@@ -275,6 +288,9 @@ class CarInterfaceBase(ABC):
if candidate == HYUNDAI.HYUNDAI_SONATA_HYBRID:
fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
if candidate == HYUNDAI.KIA_STINGER_2022 and CP.openpilotLongitudinalControl:
fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
# The refresh Elantra's safety mapping comes from the resolved Galaxy
# toggle above, not from this legacy persisted-parameter fallback.
if candidate != HYUNDAI.HYUNDAI_ELANTRA_HEV_2024 and \
+95
View File
@@ -0,0 +1,95 @@
"""One bounded Legacy AVH ON request; 0x32B is status, never a TX command."""
AVH_REQUEST = 0x6BB
AVH_STATUS = 0x32B
INPUTS = (AVH_REQUEST, AVH_STATUS, 0x40, 0x48, 0x13A, 0x174)
def checksum(address, data):
return ((address & 0xFF) + (address >> 8) + sum(data[1:])) & 0xFF
def avh_request(template, step):
if len(template) != 8 or checksum(AVH_REQUEST, template) != template[0] or template[2] & 3 or step not in (1, 2):
raise ValueError("Invalid AVH template or counter step")
data = bytearray(template)
data[1] = (data[1] & 0xF0) | ((data[1] + step) & 0xF)
data[2] |= 2
data[0] = checksum(AVH_REQUEST, data)
return AVH_REQUEST, bytes(data), 1
class AvhStartup:
def __init__(self):
self.started = None
self.last_time = None
self.stable_since = None
self.frames = {}
self.done = False
self.followup = None
def update(self, now, frames, enabled, can_valid, controls_active):
if self.started is None:
self.started = now
if self.last_time is not None and now < self.last_time:
self.done = True
self.last_time = now
if self.done:
return []
if now - self.started > 30 or controls_active:
self.done = True
return []
for address, (timestamp, data) in frames.items():
if address not in INPUTS or timestamp <= 0:
continue
previous = self.frames.get(address)
if previous and timestamp == previous[0]:
continue
if len(data) != 8 or checksum(address, data) != data[0] or timestamp > now or (previous and timestamp < previous[0]):
self.done = True
return []
if (address == AVH_REQUEST and data[2] & 3) or (address == AVH_STATUS and data[5] & 0x20) or \
(address == 0x48 and data[3] != 4) or (address == 0x40 and data[4]) or \
(address == 0x13A and any((int.from_bytes(data, 'little') >> bit) & 0x1FFF for bit in (12, 25, 38, 51))):
self.done = True
return []
if previous and (data[1] & 15) == (previous[1][1] & 15):
continue # duplicate counters cannot refresh freshness
# Controller snapshots can skip 50/100 Hz samples between updates. Panda
# checks their full counter stream; require consecutive head-unit frames here.
sequential = bool(previous and (address not in (AVH_REQUEST, AVH_STATUS) or
(data[1] & 15) == ((previous[1][1] + 1) & 15)))
self.frames[address] = (timestamp, data, sequential)
fresh = all(a in self.frames and self.frames[a][2] and
0 <= now - self.frames[a][0] <= (1.5 if a == AVH_REQUEST else 0.3) for a in INPUTS)
if not enabled or not can_valid or not fresh:
self.stable_since = None
if self.followup is not None:
self.done = True
return []
throttle = self.frames[0x40][1]
rpm = int.from_bytes(throttle[2:4], 'little') & 0x1FFF
if rpm < 400 or not self.frames[0x174][1][2] & 8:
self.stable_since = None
if self.followup is not None:
self.done = True
return []
if self.stable_since is None:
self.stable_since = now
if self.followup is not None:
sent, timestamp, template = self.followup
if now - sent > 0.075 or self.frames[AVH_REQUEST][0] != timestamp:
self.done = True
elif now - sent >= 0.05:
self.done = True
return [avh_request(template, 2)]
return []
if now - self.started < 10 or now - self.stable_since < 3:
return []
timestamp, template, _ = self.frames[AVH_REQUEST]
if now - timestamp > 0.010:
return []
self.followup = (now, timestamp, template)
return [avh_request(template, 1)]
@@ -4,6 +4,7 @@ from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, structs
from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_steer_angle_limits, apply_steer_angle_limits_vm, common_fault_avoidance
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.subaru import subarucan
from opendbc.car.subaru.avh import AvhStartup
from opendbc.car.subaru.values import CAR, DBC, GLOBAL_ES_ADDR, SUBARU_STOP_START_CARS, CanBus, CarControllerParams, SubaruFlags
from opendbc.car.vehicle_model import VehicleModel
@@ -45,7 +46,9 @@ class CarController(CarControllerBase):
self.angle_override_confirm_frames = 0
self.angle_lkas_active = False
self.angle_handoff_active = False
self.ascent_angle_initialized = False
self.ascent_aol_arm_frames = 0
self.ascent_es_distance_counter_last = None
self.cruise_button_prev = 0
self.steer_rate_counter = 0
@@ -69,6 +72,7 @@ class CarController(CarControllerBase):
self.stop_start_counter = 0
self.stop_start_acknowledged = False
self.last_redneck_button_frame = 0
self.avh_startup = AvhStartup()
def _stop_start_off_request(self, CC, CS, starpilot_toggles):
"""Send one bounded Subaru Stop/Start OFF request after ignition.
@@ -202,6 +206,10 @@ class CarController(CarControllerBase):
return subarucan.create_steering_control_angle(self.packer, apply_steer, lkas_active, self.angle_bus)
if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023 and not self.ascent_angle_initialized:
self.apply_steer_last = CS.out.steeringAngleDeg
self.ascent_angle_initialized = True
mads_only = CC.latActive and not CC.enabled
mads_only_ok = CS.out.vEgoRaw > _ANGLE_MADS_MIN_SPEED and \
abs(CS.out.steeringAngleDeg) < _ANGLE_MADS_MAX_STEER_ANGLE
@@ -215,12 +223,13 @@ class CarController(CarControllerBase):
self.ascent_aol_arm_frames = _ASCENT_AOL_ARM_FRAMES if lkas_available else 0
if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
manual_handoff = False
manual_handoff = not self.angle_lkas_active and \
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _ANGLE_REENGAGE_MAX_STEER_RATE
else:
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
lkas_active = lkas_available and not manual_handoff
if lkas_active and not self.angle_lkas_active:
if lkas_active and not self.angle_lkas_active and self.CP.carFingerprint != CAR.SUBARU_ASCENT_2023:
self.apply_steer_last = CS.out.steeringAngleDeg
apply_steer = apply_std_steer_angle_limits(
@@ -307,6 +316,13 @@ class CarController(CarControllerBase):
if stop_start_msg is not None:
can_sends.append(stop_start_msg)
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
can_sends.extend(self.avh_startup.update(
now_nanos / 1e9, getattr(CS, "avh_frames", {}),
getattr(starpilot_toggles, "subaru_avh_on", False), getattr(CS.out, "canValid", False),
CC.enabled or CC.latActive or CC.longActive,
))
# *** steering ***
if (self.frame % self.p.STEER_STEP) == 0:
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
@@ -396,7 +412,12 @@ class CarController(CarControllerBase):
can_sends.append(subarucan.create_es_distance(self.packer, self.frame // 5, CS.es_distance_msg, 0, pcm_cancel_cmd,
self.CP.openpilotLongitudinalControl, cruise_brake > 0, cruise_throttle))
else:
if pcm_cancel_cmd:
cancel_frame_ready = True
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023:
stock_counter = CS.es_distance_msg["COUNTER"]
cancel_frame_ready = stock_counter != self.ascent_es_distance_counter_last
self.ascent_es_distance_counter_last = stock_counter
if pcm_cancel_cmd and cancel_frame_ready:
if not (self.CP.flags & SubaruFlags.HYBRID):
bus = CanBus.alt_for_cp(self.CP) if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else self.main_bus
can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg["COUNTER"] + 1, CS.es_distance_msg, bus, pcm_cancel_cmd))
+11 -2
View File
@@ -4,8 +4,9 @@ from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, create_button_events, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.subaru.values import DBC, CanBus, SUBARU_REDNECK_CRUISE_CARS, SUBARU_STOP_START_CARS, SubaruFlags
from opendbc.car.subaru.values import CAR, DBC, CanBus, SUBARU_REDNECK_CRUISE_CARS, SUBARU_STOP_START_CARS, SubaruFlags
from opendbc.car import CanSignalRateCalculator
from opendbc.car.subaru.avh import INPUTS as AVH_INPUTS
ButtonType = structs.CarState.ButtonEvent.Type
@@ -26,6 +27,7 @@ class CarState(CarStateBase):
self.dashlights_msg = {}
self.dashlights_dat = b""
self.stop_start_state = 0
self.avh_frames = {}
self.cruise_buttons_msg = {}
self.cruise_buttons = {button: 0 for button in SUBARU_CRUISE_BUTTONS}
@@ -37,6 +39,9 @@ class CarState(CarStateBase):
cp_angle = cp_main if self.CP.flags & SubaruFlags.D_PLATFORM else cp
ret = structs.CarState()
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
self.avh_frames = {a: (cp_alt.ts_nanos[a]["CHECKSUM"] / 1e9, cp_alt.vl_raw[a]) for a in AVH_INPUTS}
if self.CP.carFingerprint in SUBARU_STOP_START_CARS:
stop_start_cp = cp_alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
self.dashlights_msg = copy.copy(stop_start_cp.vl["Dashlights"])
@@ -177,11 +182,15 @@ class CarState(CarStateBase):
@staticmethod
def get_can_parsers(CP):
avh_messages = [(a, 0) for a in (0x6BB, 0x32B, 0x40, 0x48)] if CP.carFingerprint == CAR.SUBARU_LEGACY_2025 else []
parsers = {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main_for_cp(CP)),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.camera),
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.alt_for_cp(CP))
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], avh_messages, CanBus.alt_for_cp(CP))
}
if CP.flags & SubaruFlags.D_PLATFORM:
parsers[Bus.main] = CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main)
if CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
for address in AVH_INPUTS:
parsers[Bus.alt].vl[address]
return parsers
@@ -42,6 +42,8 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
if candidate in SUBARU_STOP_START_CARS:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
if candidate == CAR.SUBARU_LEGACY_2025:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.AVH_STARTUP.value
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
@@ -0,0 +1,112 @@
from types import SimpleNamespace
import pytest
from opendbc.car.subaru.avh import AVH_REQUEST, AVH_STATUS, INPUTS, AvhStartup, avh_request, checksum
from opendbc.car.subaru.carcontroller import CarController
from opendbc.car.subaru.interface import CarInterface
from opendbc.car.subaru.values import CAR, SubaruSafetyFlags
from opendbc.car import Bus
def sample(address, counter):
data = bytearray(8)
data[1] = counter & 15
if address == AVH_REQUEST:
data[3], data[5], data[6] = 1, 0x80, 0x0E # captured Legacy payload, not Outback constants
elif address == 0x40:
data[2:4] = (800).to_bytes(2, 'little')
elif address == 0x48:
data[3] = 4
elif address == 0x174:
data[2] = 8
data[0] = checksum(address, data)
return bytes(data)
def prepare(fast_counter_step=1):
policy = AvhStartup()
frames = {}
for tick in range(101):
now = 100 + tick / 10
for address in INPUTS:
if address != AVH_REQUEST or tick % 10 == 0:
counter = tick // 10 if address == AVH_REQUEST else tick * (1 if address == AVH_STATUS else fast_counter_step)
frames[address] = (now, sample(address, counter))
sent = policy.update(now, frames, True, True, False)
if tick < 100:
assert sent == []
assert sent == [avh_request(frames[AVH_REQUEST][1], 1)]
return policy, frames
def test_captured_legacy_press_bytes():
template = bytes.fromhex('5b0b000100800e00')
assert avh_request(template, 1) == (0x6BB, bytes.fromhex('5e0c020100800e00'), 1)
assert avh_request(template, 2) == (0x6BB, bytes.fromhex('5f0d020100800e00'), 1)
wrap = sample(AVH_REQUEST, 15)
assert avh_request(wrap, 1)[1][1] == 0
assert avh_request(wrap, 2)[1][1] == 1
def test_two_frames_only_and_no_retry():
policy, frames = prepare()
assert policy.update(110.04, frames, True, True, False) == []
assert policy.update(110.06, frames, True, True, False) == [avh_request(frames[AVH_REQUEST][1], 2)]
assert policy.update(110.07, frames, True, True, False) == []
assert policy.update(111, frames, True, True, False) == []
def test_controller_snapshots_may_skip_fast_can_samples():
policy, frames = prepare(fast_counter_step=2)
assert policy.update(110.06, frames, True, True, False) == [avh_request(frames[AVH_REQUEST][1], 2)]
@pytest.mark.parametrize('reason', ['late', 'new_template', 'manual', 'ack', 'moving', 'gas', 'gear', 'invalid', 'disabled', 'engaged', 'stale'])
def test_followup_aborts_permanently(reason):
policy, frames = prepare()
address, offset, value = {
'manual': (AVH_REQUEST, 2, 1), 'ack': (AVH_STATUS, 5, 32),
'moving': (0x13A, 2, 1), 'gas': (0x40, 4, 1), 'gear': (0x48, 3, 3),
'new_template': (AVH_REQUEST, 1, 11),
}.get(reason, (None, None, None))
if address is not None:
data = bytearray(frames[address][1])
data[1] = (data[1] + 1) & 15
data[offset] = value
data[0] = checksum(address, data)
frames[address] = (110.05, bytes(data))
if reason == 'stale':
frames[0x40] = (109, frames[0x40][1])
now = 110.08 if reason == 'late' else 110.06
assert policy.update(now, frames, reason != 'disabled', reason != 'invalid', reason == 'engaged') == []
assert policy.done
assert policy.update(111, frames, True, True, False) == []
def test_only_legacy_has_avh_safety_permission():
for car in CAR:
cp = CarInterface.get_non_essential_params(car)
assert bool(cp.safetyConfigs[0].safetyParam & SubaruSafetyFlags.AVH_STARTUP) == (car == CAR.SUBARU_LEGACY_2025)
def test_existing_required_messages_keep_alive_checks():
cp = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
parser = CarInterface.CarState.get_can_parsers(cp)[Bus.alt]
assert not parser.message_states[0x13A].ignore_alive
assert not parser.message_states[0x174].ignore_alive
assert parser.message_states[AVH_REQUEST].ignore_alive
assert parser.message_states[AVH_STATUS].ignore_alive
def test_controller_sends_only_when_opted_in():
cp = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
controller = CarController({}, cp)
cc = SimpleNamespace(enabled=False, latActive=False, longActive=False,
actuators=SimpleNamespace(as_builder=lambda: SimpleNamespace(steeringAngleDeg=0)),
hudControl=SimpleNamespace(leadVisible=False), cruiseControl=SimpleNamespace(cancel=False))
cs = SimpleNamespace(out=SimpleNamespace(canValid=True), avh_frames={})
toggles = SimpleNamespace(subaru_stop_start_off=False, subaru_avh_on=False, subaru_sng=False)
controller.frame = 1
_, sent = controller.update(cc, cs, 100_000_000_000, toggles)
assert not any(m[0] in (AVH_REQUEST, AVH_STATUS) for m in sent)
@@ -682,6 +682,28 @@ def test_ascent_angle_controller_reengages_immediately_after_manual_steering_sto
assert CS.out.steeringAngleDeg < parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] < CC.actuators.steeringAngleDeg
def test_ascent_reentry_rate_uses_last_transmitted_angle():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
controller.angle_handoff_active = True
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-1.45))
CS = SimpleNamespace(out=SimpleNamespace(
vEgoRaw=30.3, steeringAngleDeg=0.78, steeringRateDeg=-1.5, steeringTorque=56.0,
gearShifter=structs.CarState.GearShifter.drive, standstill=False,
))
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
parser.update([(1, [controller.lateral_angle(CC, CS)])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(0.78)
CS.out.steeringAngleDeg = 0.74
CS.out.steeringRateDeg = -1.99
parser.update([(2, [controller.lateral_angle(CC, CS)])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(0.53, abs=0.01)
def test_ascent_angle_controller_waits_for_parking_lot_safety_envelope():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
@@ -806,7 +828,7 @@ def test_outback_manual_steering_keeps_cooperative_angle_request():
CS = SimpleNamespace(out=SimpleNamespace(
vEgoRaw=0.9,
steeringAngleDeg=-57.0,
steeringRateDeg=-45.0,
steeringRateDeg=0.0,
steeringTorque=0.0,
steeringPressed=True,
gearShifter=structs.CarState.GearShifter.drive,
@@ -815,6 +837,7 @@ def test_outback_manual_steering_keeps_cooperative_angle_request():
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
for frame, steering_torque in enumerate((-79.0, -81.0, -170.0, -250.0, -250.0, 79.0, 81.0, 170.0, 250.0, 250.0), start=1):
CS.out.steeringRateDeg = 0.0 if frame == 1 else -45.0
CS.out.steeringTorque = steering_torque
CS.out.steeringPressed = abs(steering_torque) > 80.0
msg = controller.lateral_angle(CC, CS)
@@ -825,6 +848,27 @@ def test_outback_manual_steering_keeps_cooperative_angle_request():
assert controller._lkas_status_active(CC)
def test_outback_waits_for_manual_turn_to_settle_before_reentry():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_OUTBACK_2023)
controller = CarController({}, CP)
CC = SimpleNamespace(enabled=False, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-80.0))
CS = SimpleNamespace(out=SimpleNamespace(
vEgoRaw=7.3, steeringAngleDeg=-121.47, steeringRateDeg=126.5,
gearShifter=structs.CarState.GearShifter.drive, standstill=False,
))
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
for frame, (angle, rate, active) in enumerate([
(-121.47, 126.5, False), (-117.96, 122.5, False), (-88.65, 112.0, False),
(-0.24, 0.0, True),
], start=1):
CS.out.steeringAngleDeg = angle
CS.out.steeringRateDeg = rate
parser.update([(frame, [controller.lateral_angle(CC, CS)])])
assert bool(parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"]) == active
if not active:
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(angle, abs=0.01)
def test_ascent_hud_waits_for_angle_request():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
@@ -835,6 +879,69 @@ def test_ascent_hud_waits_for_angle_request():
assert controller._lkas_status_active(CC)
@pytest.mark.parametrize("platform", [CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023, CAR.SUBARU_LEGACY_2025,
CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_ASCENT])
def test_stock_cruise_cancel_fresh_frame_gate_is_ascent_angle_only(platform):
CP = CarInterface.get_non_essential_params(platform)
controller = CarController({}, CP)
CC = structs.CarControl()
CC.cruiseControl.cancel = True
CS = SimpleNamespace(
out=structs.CarState(),
es_distance_msg=defaultdict(int, COUNTER=11),
es_dashstatus_msg=defaultdict(int),
es_lkas_state_msg=defaultdict(int),
es_infotainment_msg=defaultdict(int),
)
toggles = SimpleNamespace(subaru_sng=False)
cancel_messages = []
for frame in range(5):
_, sends = controller.update(CC.as_reader(), CS, frame * 10_000_000, toggles)
cancel_messages.extend(msg for msg in sends if msg[0] == 0x221)
assert len(cancel_messages) == (1 if platform == CAR.SUBARU_ASCENT_2023 else 5)
assert all(msg[2] == (CanBus.alt if CP.flags & SubaruFlags.GLOBAL_GEN2 else CanBus.main) for msg in cancel_messages)
parser = CANParser(DBC[platform][Bus.pt], [("ES_Distance", 0)], cancel_messages[0][2])
parser.update([(1, [cancel_messages[0]])])
assert parser.vl["ES_Distance"]["COUNTER"] == 12
assert parser.vl["ES_Distance"]["Cruise_Cancel"] == 1
assert parser.vl["ES_Distance"]["Cruise_Throttle"] == 1818
def test_ascent_cancel_uses_fresh_stock_frames_and_handles_counter_rollover():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
CC = structs.CarControl()
CC.latActive = True
CS = SimpleNamespace(
out=structs.CarState(vEgoRaw=24.04, steeringAngleDeg=2.93, gearShifter="drive"),
es_distance_msg=defaultdict(int, COUNTER=14),
es_dashstatus_msg=defaultdict(int),
es_lkas_state_msg=defaultdict(int),
es_infotainment_msg=defaultdict(int),
)
CS.out.cruiseState.available = True
toggles = SimpleNamespace(subaru_sng=False)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_Distance", 0)], CanBus.alt)
cancel_counters = []
for frame, (stock_counter, cancel) in enumerate([
(14, False), (14, True), (15, True), (15, True), (0, True), (0, True),
(1, False), (1, True), (2, True),
]):
CS.es_distance_msg["COUNTER"] = stock_counter
CC.cruiseControl.cancel = cancel
_, sends = controller.update(CC.as_reader(), CS, frame * 10_000_000, toggles)
cancel_messages = [msg for msg in sends if msg[0] == 0x221]
assert len(cancel_messages) <= 1
if cancel_messages:
parser.update([(frame + 1, cancel_messages)])
cancel_counters.append(parser.vl["ES_Distance"]["COUNTER"])
assert parser.vl["ES_Distance"]["Cruise_Cancel"] == 1
assert parser.vl["ES_Distance"]["Cruise_Throttle"] == 1818
assert cancel_counters == [0, 1, 3]
def test_other_angle_cars_keep_lateral_status_behavior():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
controller = CarController({}, CP)
@@ -90,6 +90,7 @@ class SubaruSafetyFlags(IntFlag):
FIXED_ANGLE_LIMITS = 128
STOP_START_BUTTON = 256
REDNECK_CRUISE = 512
AVH_STARTUP = 1024
LEGACY_2025_ANGLE_LIMITS = FIXED_ANGLE_LIMITS
@@ -153,7 +153,7 @@ class CarController(CarControllerBase):
def _update_preap(self, CC, CS):
actuators = CC.actuators
can_sends = []
lat_active = CC.latActive and CS.hands_on_level < 3
lat_active = CC.latActive and CS.hands_on_level < 3 and getattr(CS, "preap_lateral_authorized", False)
if CC.cruiseControl.cancel and CS.cruiseEnabled:
CS.cruiseEnabled = False
@@ -169,8 +169,10 @@ class CarController(CarControllerBase):
CS.engagement.pedal_speed_kph = 0.0
if self.frame % 2 == 0:
requested_angle = float(np.clip(actuators.steeringAngleDeg,
CS.out.steeringAngleDeg - 20., CS.out.steeringAngleDeg + 20.))
self.apply_angle_last = apply_steer_angle_limits_vm(
actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
requested_angle, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
lat_active, CarControllerParams, self.VM,
)
cntr = (self.frame // 2) % 16
+39 -3
View File
@@ -1,12 +1,12 @@
import copy
from cereal import custom
from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, structs
from opendbc.car import Bus, create_button_events, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.tesla.values import (
DBC, CANBUS, GEAR_MAP, STEER_DISENGAGE_THRESHOLD, STEER_THRESHOLD, TeslaSafetyFlags,
CAR, LEGACY_CARS,
CAR, LEGACY_CARS, TeslaFlags,
)
from opendbc.car.tesla.preap.carstate import get_preap_can_parsers, update_preap
from opendbc.car.tesla.preap.engagement import PreAPEngagement
@@ -19,6 +19,20 @@ TESLA_GAS_PRESS_ON = 0.8
TESLA_GAS_PRESS_OFF = 0.4
class TeslaScreenCANParser(CANParser):
def __init__(self):
super().__init__("tesla_model3_vehicle", [("UI_status2", 0), ("SCCM_rightStalk", 0)], CANBUS.vehicle)
def update(self, strings, sendcan=False):
if strings and not isinstance(strings[0], list | tuple):
strings = [strings]
# Match Panda's exact-length check before producing engagement button events.
return super().update([
(timestamp, [frame for frame in frames if (frame[0], len(frame[1])) in ((0x3DF, 8), (0x229, 3))])
for timestamp, frames in strings
], sendcan)
def update_tesla_gas_pressed(previous: bool, pedal_position: float) -> bool:
threshold = TESLA_GAS_PRESS_OFF if previous else TESLA_GAS_PRESS_ON
return float(pedal_position) > threshold
@@ -51,6 +65,8 @@ class CarState(CarStateBase):
self.cruise_buttons = 0
self.prev_cruise_buttons = 0
self.gas_pressed = False
self.active_touch_points = None
self.screen_cancel_pressed = False
self.msg_stw_actn_req = None
self.speed_units = "MPH"
self.cooperative_steering = any(
@@ -86,6 +102,20 @@ class CarState(CarStateBase):
return False
return super().update_button_enable(buttonEvents)
def update_screen_button(self, cp_vehicle):
events = []
for stalk_status in cp_vehicle.vl_all["SCCM_rightStalk"]["SCCM_rightStalkStatus"]:
cancel_pressed = int(stalk_status) in (1, 2) # UP_1, UP_2
events.extend(create_button_events(cancel_pressed, self.screen_cancel_pressed, {True: ButtonType.cancel}))
self.screen_cancel_pressed = cancel_pressed
for touch_points in cp_vehicle.vl_all["UI_status2"]["UI_activeTouchPoints"]:
touch_points = int(touch_points)
# Establish a baseline first; a touch already held during boot is not an engagement request.
if self.active_touch_points is not None:
events.extend(create_button_events(touch_points, self.active_touch_points, {3: ButtonType.lkas}))
self.active_touch_points = touch_points
return events
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
if self.CP.carFingerprint == CAR.TESLA_MODEL_S_PREAP:
return update_preap(self, can_parsers)
@@ -181,11 +211,15 @@ class CarState(CarStateBase):
else:
pass
# Buttons # ToDo: add Gap adjust button
if self.CP.flags & TeslaFlags.AOL_SCREEN_BUTTON:
ret.buttonEvents = list(ret.buttonEvents) + self.update_screen_button(can_parsers[Bus.adas])
# Messages needed by carcontroller
self.das_control = copy.copy(cp_ap_party.vl["DAS_control"])
fp_ret = custom.StarPilotCarState.new_message()
if self.CP.flags & TeslaFlags.AOL_SCREEN_BUTTON:
fp_ret.cancelPressed = self.screen_cancel_pressed
return ret, fp_ret
@@ -279,5 +313,7 @@ class CarState(CarStateBase):
}
return {
Bus.party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.party),
Bus.ap_party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.autopilot_party)
Bus.ap_party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.autopilot_party),
**({Bus.adas: TeslaScreenCANParser()}
if CP.flags & TeslaFlags.AOL_SCREEN_BUTTON else {}),
}
+10 -1
View File
@@ -3,7 +3,7 @@ from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.tesla.carcontroller import CarController
from opendbc.car.tesla.carstate import CarState
from opendbc.car.tesla.radar_interface import RadarInterface
from opendbc.car.tesla.values import TeslaSafetyFlags, CAR, DBC, LEGACY_CARS
from opendbc.car.tesla.values import TeslaFlags, TeslaSafetyFlags, CANBUS, CAR, DBC, LEGACY_CARS
from opendbc.car.tesla.preap.interface import get_preap_accel_limits, get_preap_params
@@ -23,6 +23,12 @@ class CarInterface(CarInterfaceBase):
ret = super().get_params(candidate, fingerprint, car_fw, alpha_long, is_release, docs, starpilot_toggles)
if candidate == CAR.TESLA_MODEL_3 and getattr(starpilot_toggles, "tesla_cooperative_steering", False):
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.COOP_STEERING.value
if (ret.flags & TeslaFlags.HAS_VEHICLE_BUS and
getattr(starpilot_toggles, "tesla_aol_screen_tap_requested", False)):
ret.flags |= TeslaFlags.AOL_SCREEN_BUTTON.value
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.AOL_SCREEN_BUTTON.value
if getattr(starpilot_toggles, "tesla_aol_screen_brake_disengage_requested", False):
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.AOL_SCREEN_DISENGAGE_ON_BRAKE.value
return ret
@staticmethod
@@ -49,6 +55,9 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla)]
if candidate in (CAR.TESLA_MODEL_3, CAR.TESLA_MODEL_Y) and fingerprint[CANBUS.vehicle].get(0x3DF) == 8:
ret.flags |= TeslaFlags.HAS_VEHICLE_BUS.value
ret.steerLimitTimer = 0.4
ret.steerActuatorDelay = 0.1
ret.steerAtStandstill = True
@@ -13,6 +13,8 @@ class PreAPEngagement:
self.enableDoublePull = double_pull_enabled
self.double_pull_window_ms = double_pull_window_ms
self.cruiseEnabled = False
self.lateralEnabled = False
self.lateralRearmRequired = False
self.enableLongControl = False
self.enableJustCC = False
self.pending_enable = False
@@ -28,6 +30,8 @@ class PreAPEngagement:
def handle_steering_disengage(self, steering_disengage: bool) -> None:
if steering_disengage and not self.prev_steering_disengage:
self.lateralEnabled = False
self.lateralRearmRequired = True
self.cruiseEnabled = False
self.enableLongControl = False
self.enableJustCC = False
@@ -45,6 +49,8 @@ class PreAPEngagement:
button_events: list[structs.CarState.ButtonEvent] = []
if cruise_buttons == CruiseButtons.MAIN and prev_cruise_buttons != CruiseButtons.MAIN:
self.lateralEnabled = True
self.lateralRearmRequired = False
if self.enableDoublePull:
self._handle_double_pull(curr_time_ms, v_ego, speed_units, use_pedal, pedal_long_allowed, long_control_allowed, di_cruise_state)
else:
@@ -75,6 +81,8 @@ class PreAPEngagement:
def check_can_engage(self, door_open: bool, gear_shifter, seatbelt_unlatched: bool) -> bool:
can_engage = not door_open and gear_shifter == structs.CarState.GearShifter.drive and not seatbelt_unlatched
if not can_engage:
self.lateralEnabled = False
self.lateralRearmRequired = True
self.cruiseEnabled = False
self.enableLongControl = False
self.enableJustCC = False
@@ -118,6 +126,8 @@ class PreAPEngagement:
((curr_time_ms - self.preap_last_cc_spoof_ms) < SPOOF_ECHO_WINDOW_MS)
be.type = ButtonType.unknown if is_echo else ButtonType.cancel
if not is_echo:
self.lateralEnabled = False
self.lateralRearmRequired = True
self.cruiseEnabled = False
self.enableLongControl = False
self.enableJustCC = False
@@ -145,4 +155,3 @@ class PreAPEngagement:
def _capture_target_speed(v_ego: float, speed_units: str) -> float:
speed_uom_kph = CV.MPH_TO_KPH if speed_units == "MPH" else 1.0
return max(int(v_ego * CV.MS_TO_KPH / speed_uom_kph + 0.5) * speed_uom_kph, 0.0)
@@ -0,0 +1,21 @@
from opendbc.car import structs
from opendbc.safety import ALTERNATIVE_EXPERIENCE
def preap_lateral_authorized(CP, CS, panda_states, panda_states_valid: bool) -> bool:
"""Match Pre-AP's existing safety authorization without treating software CC availability as ACC main."""
if not panda_states_valid or CS.out.gearShifter != structs.CarState.GearShifter.drive or CS.out.doorOpen or CS.out.steeringDisengage:
return False
if CS.engagement.lateralRearmRequired:
return False
config = CP.safetyConfigs[0]
matching = [p for p in panda_states if p.safetyModel == config.safetyModel and p.safetyParam == config.safetyParam]
if len(matching) != 1 or matching[0].safetyRxChecksInvalid:
return False
panda = matching[0]
# Physical cancel/override/gear changes clear this latch immediately, whereas
# Panda telemetry can lag. Longitudinal software cancellation leaves it intact.
stalk_authorized = CS.engagement.lateralEnabled and panda.controlsAllowed
stock_main = CS.di_cruise_state in ("STANDBY", "ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL")
aol_authorized = bool(panda.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL) and stock_main
return bool(stalk_authorized or aol_authorized)
@@ -0,0 +1,36 @@
from types import SimpleNamespace
import pytest
from opendbc.car import structs
from opendbc.car.tesla.carcontroller import CarController
from opendbc.car.tesla.interface import CarInterface
from opendbc.car.tesla.values import CAR, DBC
@pytest.mark.parametrize('direction', [-1., 1.])
def test_preap_stalled_rack_request_stays_within_legacy_tracking_envelope(direction):
cp = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP)
controller = CarController(DBC[cp.carFingerprint], cp)
controller.stock_cc = None
cs = SimpleNamespace(out=SimpleNamespace(vEgoRaw=3., steeringAngleDeg=0.),
hands_on_level=0, preap_lateral_authorized=True, cruiseEnabled=False)
cc = structs.CarControl.new_message()
cc.latActive = True
cc.actuators.steeringAngleDeg = direction * 100.
previous = 0.
for frame in range(100):
output, _ = controller.update(cc.as_reader(), cs, frame * 10000000, None)
assert abs(output.steeringAngleDeg) <= 20.
assert abs(output.steeringAngleDeg - previous) <= 5.
previous = output.steeringAngleDeg
assert previous == direction * 20.
cs.out.steeringAngleDeg = -direction * 50.
output, _ = controller.update(cc.as_reader(), cs, 1000000000, None)
assert abs(output.steeringAngleDeg - previous) <= 5.
cc.latActive = False
controller.frame = 102
output, _ = controller.update(cc.as_reader(), cs, 1020000000, None)
assert output.steeringAngleDeg == cs.out.steeringAngleDeg
@@ -0,0 +1,155 @@
from types import SimpleNamespace
import pytest
from cereal import custom
from opendbc.can import CANPacker
from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.car.tesla.carstate import ButtonType, CarState
from opendbc.car.tesla.interface import CarInterface
from opendbc.car.tesla.values import CANBUS, CAR, TeslaFlags, TeslaSafetyFlags
def screen_params(candidate=CAR.TESLA_MODEL_3, enabled=True, bus=CANBUS.vehicle, length=8, alpha_long=False):
fingerprint = gen_empty_fingerprint()
if bus is not None:
fingerprint[bus][0x3DF] = length
toggles = SimpleNamespace(tesla_aol_screen_tap_requested=enabled, trailer_load_kg=0.0)
return CarInterface.get_params(candidate, fingerprint, [], alpha_long, False, False, toggles)
@pytest.mark.parametrize("candidate", tuple(CAR))
@pytest.mark.parametrize("enabled", (False, True))
def test_detection_and_safety_flags_are_model_3_y_only(candidate, enabled):
cp = screen_params(candidate, enabled)
detected = candidate in (CAR.TESLA_MODEL_3, CAR.TESLA_MODEL_Y)
assert bool(cp.flags & TeslaFlags.HAS_VEHICLE_BUS) == detected
assert bool(cp.flags & TeslaFlags.AOL_SCREEN_BUTTON) == (detected and enabled)
assert any(config.safetyParam & TeslaSafetyFlags.AOL_SCREEN_BUTTON for config in cp.safetyConfigs) == (detected and enabled)
@pytest.mark.parametrize(("bus", "length"), ((None, 8), (0, 8), (2, 8), (1, 7), (1, 4)))
def test_harness_requires_correct_bus_and_length(bus, length):
cp = screen_params(bus=bus, length=length)
assert not cp.flags & (TeslaFlags.HAS_VEHICLE_BUS | TeslaFlags.AOL_SCREEN_BUTTON)
assert not cp.safetyConfigs[0].safetyParam & TeslaSafetyFlags.AOL_SCREEN_BUTTON
assert Bus.adas not in CarState.get_can_parsers(cp)
@pytest.mark.parametrize("alpha_long", (False, True))
def test_gesture_does_not_select_longitudinal(alpha_long):
baseline = screen_params(enabled=False, alpha_long=alpha_long)
enabled = screen_params(alpha_long=alpha_long)
assert enabled.openpilotLongitudinalControl == baseline.openpilotLongitudinalControl
assert enabled.pcmCruise == baseline.pcmCruise
assert enabled.safetyConfigs[0].safetyParam == baseline.safetyConfigs[0].safetyParam | TeslaSafetyFlags.AOL_SCREEN_BUTTON
@pytest.mark.parametrize("enabled", (False, True))
def test_screen_brake_safety_flag_requires_the_gesture_feature(enabled):
fingerprint = gen_empty_fingerprint()
fingerprint[1][0x3DF] = 8
toggles = SimpleNamespace(tesla_aol_screen_tap_requested=enabled, tesla_aol_screen_brake_disengage_requested=True,
trailer_load_kg=0.0)
cp = CarInterface.get_params(CAR.TESLA_MODEL_3, fingerprint, [], False, False, False, toggles)
assert bool(cp.safetyConfigs[0].safetyParam & TeslaSafetyFlags.AOL_SCREEN_DISENGAGE_ON_BRAKE) == enabled
def test_parser_only_added_with_setting_and_detected_addon():
assert Bus.adas not in CarState.get_can_parsers(screen_params(enabled=False))
parser = CarState.get_can_parsers(screen_params())[Bus.adas]
assert parser.bus == CANBUS.vehicle
assert parser.message_states[0x3DF].ignore_alive
@pytest.mark.parametrize(("counts", "expected"), (
([0, 3, 3, 3, 0, 0, 3, 0], [True, False, True, False]),
([3, 3, 0, 3, 0], [False, True, False]),
([0, 1, 2, 4, 5, 0], []),
))
def test_touch_edges_and_startup_baseline(counts, expected):
cp = screen_params()
fp_cp = custom.StarPilotCarParams.new_message()
cs = CarState(cp, fp_cp)
parser = CarState.get_can_parsers(cp)[Bus.adas]
packer = CANPacker("tesla_model3_vehicle")
events = []
for i, count in enumerate(counts):
parser.update([[int((i + 1) * 0.5e9), [packer.make_can_msg("UI_status2", CANBUS.vehicle, {"UI_activeTouchPoints": count})]]])
events.extend(cs.update_screen_button(parser))
assert [event.pressed for event in events if event.type == ButtonType.lkas] == expected
parser.update([[60_000_000_000, []]])
assert cs.update_screen_button(parser) == []
assert parser.can_valid
assert not parser.bus_timeout
def test_multiple_touch_edges_in_one_update_are_not_lost():
cp = screen_params()
fp_cp = custom.StarPilotCarParams.new_message()
cs = CarState(cp, fp_cp)
parser = CarState.get_can_parsers(cp)[Bus.adas]
packer = CANPacker("tesla_model3_vehicle")
parser.update([[i * 10_000_000, [packer.make_can_msg("UI_status2", 1, {"UI_activeTouchPoints": count})]]
for i, count in enumerate((0, 3, 0, 3, 0), 1)])
assert [event.pressed for event in cs.update_screen_button(parser) if event.type == ButtonType.lkas] == [True, False, True, False]
@pytest.mark.parametrize("length", (0, 3, 4, 5, 6, 7, 9, 12))
def test_malformed_touch_frames_do_not_produce_button_events(length):
cp = screen_params()
cs = CarState(cp, custom.StarPilotCarParams.new_message())
parser = CarState.get_can_parsers(cp)[Bus.adas]
packer = CANPacker("tesla_model3_vehicle")
parser.update([[1_000_000_000, [packer.make_can_msg("UI_status2", 1, {"UI_activeTouchPoints": 0})]]])
cs.update_screen_button(parser)
address, data, bus = packer.make_can_msg("UI_status2", 1, {"UI_activeTouchPoints": 3})
parser.update([[2_000_000_000, [(address, data[:length].ljust(length, b"\x00"), bus)]]])
assert cs.update_screen_button(parser) == []
assert cs.active_touch_points == 0
parser.update([[3_000_000_000, [(address, data, bus)]]])
assert any(event.type == ButtonType.lkas and event.pressed for event in cs.update_screen_button(parser))
def test_button_is_appended_without_changing_cruise_state():
cp = screen_params()
fp_cp = custom.StarPilotCarParams.new_message()
cs = CarState(cp, fp_cp)
parsers = CarState.get_can_parsers(cp)
parsers[Bus.party].vl["DI_state"]["DI_cruiseState"] = 0
packer = CANPacker("tesla_model3_vehicle")
for count in (0, 3):
parsers[Bus.adas].update([[1_000_000_000, [packer.make_can_msg("UI_status2", 1, {"UI_activeTouchPoints": count})]]])
ret, _ = cs.update(parsers, SimpleNamespace())
assert any(event.type == ButtonType.lkas and event.pressed for event in ret.buttonEvents)
assert not ret.cruiseState.enabled
assert not ret.cruiseState.available
def test_addon_stalk_cancel_events_and_held_state():
cp = screen_params()
cs = CarState(cp, custom.StarPilotCarParams.new_message())
parsers = CarState.get_can_parsers(cp)
packer = CANPacker("tesla_model3_vehicle")
events = []
for i, status in enumerate((0, 1, 1, 2, 0, 3, 4, 0, 2, 0), 1):
parsers[Bus.adas].update([[i * 10_000_000, [packer.make_can_msg("SCCM_rightStalk", 1, {"SCCM_rightStalkStatus": status})]]])
ret, fp_ret = cs.update(parsers, SimpleNamespace())
assert fp_ret.cancelPressed == (status in (1, 2))
events.extend(ret.buttonEvents)
assert [event.pressed for event in events if event.type == ButtonType.cancel] == [True, False, True, False]
parsers[Bus.adas].update([[60_000_000_000, []]])
assert parsers[Bus.adas].can_valid
assert not parsers[Bus.adas].bus_timeout
@pytest.mark.parametrize(("bus", "length"), ((0, 3), (2, 3), (1, 2), (1, 4), (1, 8)))
def test_wrong_bus_or_length_stalk_frames_do_not_cancel(bus, length):
cp = screen_params()
cs = CarState(cp, custom.StarPilotCarParams.new_message())
parser = CarState.get_can_parsers(cp)[Bus.adas]
packer = CANPacker("tesla_model3_vehicle")
address, data, _ = packer.make_can_msg("SCCM_rightStalk", bus, {"SCCM_rightStalkStatus": 1})
parser.update([[1_000_000_000, [(address, data[:length].ljust(length, b"\x00"), bus)]]])
assert cs.update_screen_button(parser) == []
assert not cs.screen_cancel_pressed
+4
View File
@@ -144,10 +144,14 @@ class TeslaSafetyFlags(IntFlag):
FLAG_EXTERNAL_PANDA = 4
FLAG_HW1 = 8
COOP_STEERING = 256
AOL_SCREEN_BUTTON = 512
AOL_SCREEN_DISENGAGE_ON_BRAKE = 1024
class TeslaFlags(IntFlag):
LONG_CONTROL = 1
HAS_VEHICLE_BUS = 2
AOL_SCREEN_BUTTON = 4
class CruiseButtons:
@@ -10,8 +10,8 @@ from opendbc.car.secoc import add_mac, build_sync_mac
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.toyota import toyotacan
from opendbc.car.toyota.values import CAR, MIN_ACC_SPEED, NO_STOP_TIMER_CAR, PEDAL_TRANSITION, TSS2_CAR, \
CarControllerParams, ToyotaFlags, \
UNSUPPORTED_DSU_CAR, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS
CarControllerParams, ToyotaFlags, ToyotaSafetyFlags, \
UNSUPPORTED_DSU_CAR, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS, uses_toyota_auto_hold_aeb
from opendbc.can import CANPacker
Ecu = structs.CarParams.Ecu
@@ -47,6 +47,7 @@ TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES = 100
# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
MAX_STEER_RATE = 100 # deg/s
MAX_STEER_RATE_FRAMES = 17 # tx control frames needed before torque can be cut
COROLLA_MAX_STEER_RATE = 80
# EPS allows user torque above threshold for 50 frames before permanently faulting
MAX_USER_TORQUE = 500
@@ -67,6 +68,11 @@ def is_ths_hybrid(CP) -> bool:
return CP.carFingerprint in LEGACY_PRIUS_CAR or is_camry_hybrid(CP)
def uses_rav4_hybrid_sdsu_longitudinal(CP) -> bool:
return bool(CP.carFingerprint == CAR.TOYOTA_RAV4H and CP.openpilotLongitudinalControl and
CP.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.LONG_FILTER.value)
def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool:
highlander_sdsu = (
CP.carFingerprint == CAR.TOYOTA_HIGHLANDER and
@@ -77,12 +83,12 @@ def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool:
) or highlander_sdsu)
def get_toyota_lat_active(car_fingerprint, requested_active: bool, steering_torque: float,
steering_pressed: bool) -> bool:
if not requested_active or abs(steering_torque) >= MAX_USER_TORQUE:
return False
def get_toyota_lat_active(requested_active: bool, steering_torque: float) -> bool:
return requested_active and abs(steering_torque) < MAX_USER_TORQUE
return not (car_fingerprint == CAR.TOYOTA_COROLLA_TSS2 and steering_pressed)
def get_toyota_steer_rate_limit(car_fingerprint) -> int:
return COROLLA_MAX_STEER_RATE if car_fingerprint == CAR.TOYOTA_COROLLA_TSS2 else MAX_STEER_RATE
def supports_toyota_auto_hold(CP, auto_hold_enabled: bool) -> bool:
@@ -106,7 +112,7 @@ def get_long_tune(CP, params):
kiV = [0.5, 0.25]
k_f = 1.0
if is_ths_hybrid(CP):
if is_ths_hybrid(CP) or uses_rav4_hybrid_sdsu_longitudinal(CP):
k_f = 0.8 if CP.carFingerprint in LEGACY_PRIUS_CAR else 1.0
elif CP.carFingerprint not in TSS2_CAR:
kiBP = [0., 5., 35.]
@@ -252,6 +258,7 @@ class CarController(CarControllerBase):
self.standstill_req = False
self.permit_braking = True
self.steer_rate_counter = 0
self.steer_rate_limit = get_toyota_steer_rate_limit(self.CP.carFingerprint)
self.distance_button = 0
# *** start long control state ***
@@ -276,7 +283,7 @@ class CarController(CarControllerBase):
self._brake_hold_counter = 0
def _compute_interceptor_gas_cmd(self, CC, CS):
if not (self.CP.enableGasInterceptorDEPRECATED and self.CP.openpilotLongitudinalControl and CC.longActive):
if self.brake_hold_active or not (self.CP.enableGasInterceptorDEPRECATED and self.CP.openpilotLongitudinalControl and CC.longActive):
return 0.0
if CS.out.standstill:
@@ -325,16 +332,36 @@ class CarController(CarControllerBase):
not CS.out.gasPressed and not CS.out.cruiseState.enabled and
CS.out.gearShifter not in (PARK, REVERSE))
if not brake_hold_allowed:
self.reset_auto_hold_state()
elif not self.brake_hold_active:
if CS.out.brakePressed:
self._brake_hold_counter += 1
self.brake_hold_active = self._brake_hold_counter > activation_frames
else:
self._brake_hold_counter = 0
return self.brake_hold_active
def create_auto_brake_hold_messages(self, CS: structs.CarState, brake_hold_allowed_timer: int = 100):
brake_hold_allowed = (CS.out.standstill and CS.out.cruiseState.available and
not CS.out.gasPressed and not CS.out.cruiseState.enabled and
CS.out.gearShifter not in (PARK, REVERSE))
if brake_hold_allowed and not self.brake_hold_active and CS.out.brakePressed:
self._brake_hold_counter += 1
self.brake_hold_active = self._brake_hold_counter > activation_frames
self.brake_hold_active = self._brake_hold_counter > brake_hold_allowed_timer
elif not brake_hold_allowed:
self._brake_hold_counter = 0
self.brake_hold_active = False
return self.brake_hold_active
if self.frame % 2 == 0:
return [toyotacan.create_brake_hold_command(self.packer, self.frame, CS.pre_collision_2, self.brake_hold_active)]
return []
def reset_auto_hold_state(self):
if self.brake_hold_active and not uses_toyota_auto_hold_aeb(self.CP):
self.standstill_req = False
self._brake_hold_counter = 0
self.brake_hold_active = False
@@ -343,8 +370,7 @@ class CarController(CarControllerBase):
stopping = actuators.longControlState == LongCtrlState.stopping
hud_control = CC.hudControl
pcm_cancel_cmd = CC.cruiseControl.cancel
lat_active = get_toyota_lat_active(self.CP.carFingerprint, CC.latActive,
CS.out.steeringTorque, CS.out.steeringPressed)
lat_active = get_toyota_lat_active(CC.latActive, CS.out.steeringTorque)
if len(CC.orientationNED) == 3:
self.pitch.update(CC.orientationNED[1])
@@ -371,7 +397,7 @@ class CarController(CarControllerBase):
# >100 degree/sec steering fault prevention
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(
abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
abs(CS.out.steeringRateDeg) >= self.steer_rate_limit, lat_active,
self.steer_rate_counter, MAX_STEER_RATE_FRAMES,
)
@@ -435,7 +461,10 @@ class CarController(CarControllerBase):
self._update_standstill_request(CC, CS, actuators, starpilot_toggles)
if supports_toyota_auto_hold(self.CP, getattr(starpilot_toggles, "toyota_auto_hold", False)):
self.update_auto_hold_state(CS, pcm_cancel_cmd)
if uses_toyota_auto_hold_aeb(self.CP):
can_sends.extend(self.create_auto_brake_hold_messages(CS))
else:
self.update_auto_hold_state(CS, pcm_cancel_cmd)
else:
self.reset_auto_hold_state()
@@ -545,7 +574,7 @@ class CarController(CarControllerBase):
pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX))
if self.brake_hold_active:
if self.brake_hold_active and not uses_toyota_auto_hold_aeb(self.CP):
pcm_accel_cmd = TOYOTA_AUTO_HOLD_ACCEL
self.permit_braking = True
self.standstill_req = True
@@ -9,6 +9,7 @@ from opendbc.car.interfaces import CarStateBase
from opendbc.car.toyota.values import ToyotaFlags, ToyotaStarPilotFlags, CAR, DBC, STEER_THRESHOLD, NO_STOP_TIMER_CAR, \
TSS2_CAR, RADAR_ACC_CAR, EPS_SCALE, UNSUPPORTED_DSU_CAR, \
SECOC_CAR, LEGACY_PRIUS_CAR
from opendbc.safety import ALTERNATIVE_EXPERIENCE
ButtonType = structs.CarState.ButtonEvent.Type
SteerControlType = structs.CarParams.SteerControlType
@@ -90,6 +91,11 @@ class CarState(CarStateBase):
self.has_can_filter = self.FPCP.flags & ToyotaStarPilotFlags.RADAR_CAN_FILTER.value
self.has_SDSU = self.FPCP.flags & ToyotaStarPilotFlags.SMART_DSU.value
self.has_ZSS = self.FPCP.flags & ToyotaStarPilotFlags.ZSS.value
self.auto_brake_hold = bool(
self.CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value and
getattr(self.CP, "alternativeExperience", 0) & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
)
self.pre_collision_2 = {}
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -225,6 +231,9 @@ class CarState(CarStateBase):
if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD"])
if self.auto_brake_hold:
self.pre_collision_2 = copy.copy(cp_cam.vl["PRE_COLLISION_2"])
if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR:
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
@@ -309,6 +318,10 @@ class CarState(CarStateBase):
if CP.carFingerprint in DISTANCE_BUTTON_CAR:
pt_messages.append(("PCM_CRUISE_4", 1))
if (CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value and
getattr(CP, "alternativeExperience", 0) & ALTERNATIVE_EXPERIENCE.ALLOW_AEB):
cam_messages.append(("PRE_COLLISION_2", 50))
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2),
+10 -6
View File
@@ -1,10 +1,11 @@
from opendbc.car import Bus, structs, get_safety_config, uds
from opendbc.car.toyota.carstate import CarState
from opendbc.car.toyota.carcontroller import CarController
from opendbc.car.toyota.carcontroller import CarController, uses_rav4_hybrid_sdsu_longitudinal
from opendbc.car.toyota.radar_interface import RadarInterface
from opendbc.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \
MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR, \
ToyotaSafetyFlags, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS
ToyotaSafetyFlags, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS, \
uses_toyota_auto_hold_aeb
from opendbc.car.disable_ecu import disable_ecu
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.safety import ALTERNATIVE_EXPERIENCE
@@ -101,7 +102,7 @@ class CarInterface(CarInterfaceBase):
# https://engage.toyota.com/static/images/toyota_safety_sense/TSS_Applicability_Chart.pdf
stop_and_go = candidate != CAR.TOYOTA_AVALON
elif candidate in (CAR.TOYOTA_RAV4_TSS2, CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023, CAR.TOYOTA_RAV4_PRIME):
elif candidate in ANGLE_CONTROL_CAR:
ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kiBP = [0.0]
ret.lateralTuning.pid.kpBP = [0.0]
@@ -165,7 +166,9 @@ class CarInterface(CarInterfaceBase):
toyota_auto_hold = Params(return_defaults=True).get_bool("ToyotaAutoHold")
if toyota_auto_hold and ret.openpilotLongitudinalControl and candidate in TOYOTA_AUTO_HOLD_CARS:
ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
ret.alternativeExperience |= (ALTERNATIVE_EXPERIENCE.ALLOW_AEB
if uses_toyota_auto_hold_aeb(ret)
else ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD)
ret.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value
if not ret.openpilotLongitudinalControl:
@@ -173,7 +176,8 @@ class CarInterface(CarInterfaceBase):
# min speed to enable ACC. if car can do stop and go, then set enabling speed
# to a negative value, so it won't matter.
ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptorDEPRECATED) else MIN_ACC_SPEED
rav4_hybrid_sdsu_long_defaults = uses_rav4_hybrid_sdsu_longitudinal(ret)
ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptorDEPRECATED or rav4_hybrid_sdsu_long_defaults) else MIN_ACC_SPEED
prius_long_defaults = candidate in LEGACY_PRIUS_CAR and ret.openpilotLongitudinalControl
camry_hybrid_long_defaults = (candidate == CAR.TOYOTA_CAMRY and ret.openpilotLongitudinalControl and
@@ -190,7 +194,7 @@ class CarInterface(CarInterfaceBase):
if ret.flags & ToyotaFlags.HYBRID.value:
ret.longitudinalActuatorDelay = 0.05
if camry_hybrid_long_defaults:
if camry_hybrid_long_defaults or rav4_hybrid_sdsu_long_defaults:
# The THS eCVT responds much faster than the legacy non-TSS2 ICE tune.
ret.longitudinalActuatorDelay = 0.05
ret.vEgoStopping = 0.25
@@ -6,12 +6,14 @@ from hypothesis import given, settings, strategies as st
from opendbc.car import Bus, structs
from opendbc.can import CANPacker, CANParser
from opendbc.car.structs import CarParams
from opendbc.car.lateral import common_fault_avoidance
from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car
from opendbc.car.toyota import toyotacan
from opendbc.car.toyota.carcontroller import CarController, get_camry_hybrid_feedforward, get_long_tune, get_prius_feedforward, \
get_prius_positive_feedforward_scale, \
get_rav4_interceptor_pedal_scale, \
get_toyota_lat_active, \
get_toyota_lat_active, get_toyota_steer_rate_limit, \
MAX_STEER_RATE, MAX_STEER_RATE_FRAMES, MAX_USER_TORQUE, \
limit_interceptor_pcm_accel, \
limit_interceptor_stopping_accel, limit_no_lead_cruise_sign_flip, \
limit_prius_stopping_accel, should_bypass_toyota_long_pid, supports_toyota_auto_hold, \
@@ -23,6 +25,7 @@ from opendbc.car.toyota.radar_interface import RadarInterface, TSSP_RADAR_EGO_SP
from opendbc.car.toyota.values import CAR, DBC, MIN_ACC_SPEED, TSS2_CAR, ANGLE_CONTROL_CAR, RADAR_ACC_CAR, SECOC_CAR, \
FW_QUERY_CONFIG, PLATFORM_CODE_ECUS, FUZZY_EXCLUDED_PLATFORMS, \
ToyotaFlags, ToyotaSafetyFlags, ToyotaStarPilotFlags, TOYOTA_AUTO_HOLD_CARS, \
uses_toyota_auto_hold_aeb, \
get_platform_codes
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from openpilot.common.params import Params
@@ -82,11 +85,48 @@ class TestToyotaInterfaces:
SimpleNamespace(force_torque_controller=True, nnff=False, nnff_lite=False),
)
assert default_params.lateralTuning.which() == "pid"
assert default_params.lateralTuning.which() == "torque"
assert forced_params.lateralTuning.which() == "torque"
assert default_params.lateralTuning.to_dict() == forced_params.lateralTuning.to_dict()
assert forced_params.lateralTuning.torque.latAccelFactor == pytest.approx(1.7)
assert forced_params.lateralTuning.torque.friction == pytest.approx(0.14)
@pytest.mark.parametrize("candidate", list(CAR))
def test_toyota_default_controller_preserves_native_angle_path(self, candidate):
fingerprint = {bus: {} for bus in range(8)}
default_params = CarInterface.get_params(
candidate, fingerprint, [], False, False, False,
SimpleNamespace(force_torque_controller=False, nnff=False, nnff_lite=False),
)
forced_params = CarInterface.get_params(
candidate, fingerprint, [], False, False, False,
SimpleNamespace(force_torque_controller=True, nnff=False, nnff_lite=False),
)
if candidate in ANGLE_CONTROL_CAR:
assert default_params.steerControlType == CarParams.SteerControlType.angle
assert default_params.lateralTuning.which() == "pid"
assert default_params.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.LTA.value
else:
assert default_params.steerControlType == CarParams.SteerControlType.torque
assert default_params.lateralTuning.which() == "torque"
assert not default_params.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.LTA.value
assert default_params.to_dict() == forced_params.to_dict()
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_RAV4_TSS2, CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_PRIME])
@pytest.mark.parametrize("eps_version", [b'\x028965B0R01400', b'8965B42181\x00\x00\x00\x00\x00\x00', b'8965B0R01200'])
def test_rav4_rack_variants_default_to_torque(self, candidate, eps_version):
params = CarInterface.get_params(
candidate, {bus: {} for bus in range(8)}, [CarParams.CarFw(ecu=Ecu.eps, fwVersion=eps_version)],
False, False, False,
SimpleNamespace(force_torque_controller=False, nnff=False, nnff_lite=False),
)
assert params.lateralTuning.which() == "torque"
assert params.steerActuatorDelay == pytest.approx(0.12)
assert params.steerControlType == CarParams.SteerControlType.torque
def test_prius_force_torque_controller_preserves_vehicle_tune(self):
fingerprint = {bus: {} for bus in range(8)}
car_fw = [CarParams.CarFw(ecu=Ecu.eps, fwVersion=b'8965B47050\x00\x00\x00\x00\x00\x00')]
@@ -190,8 +230,15 @@ class TestToyotaInterfaces:
if car_model in TSS2_CAR and car_model not in SECOC_CAR:
assert dbc[Bus.pt] == "toyota_nodsu_pt_generated"
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H])
def test_auto_hold_sets_flag_on_supported_toyota(self, candidate):
@pytest.mark.parametrize("candidate,hybrid", [
(CAR.TOYOTA_CAMRY_TSS2, False),
(CAR.TOYOTA_RAV4, False),
(CAR.TOYOTA_RAV4H, True),
(CAR.TOYOTA_RAV4_TSS2, False),
(CAR.TOYOTA_RAV4_TSS2, True),
(CAR.TOYOTA_COROLLA_TSS2, True),
])
def test_auto_hold_sets_flag_on_supported_toyota(self, candidate, hybrid):
params = Params()
try:
params.put_bool("ToyotaAutoHold", True)
@@ -199,7 +246,7 @@ class TestToyotaInterfaces:
candidate,
{bus: ({0x2FF: 8} if candidate in (CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H) and bus == 0 else {})
for bus in range(8)},
[],
[CarParams.CarFw(ecu=Ecu.hybrid, address=0x7D2, fwVersion=b"test")] if hybrid else [],
alpha_long=False,
is_release=False,
docs=False,
@@ -209,22 +256,36 @@ class TestToyotaInterfaces:
params.remove("ToyotaAutoHold")
assert car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
legacy_hold = candidate == CAR.TOYOTA_CAMRY_TSS2 or (candidate == CAR.TOYOTA_RAV4_TSS2 and hybrid)
assert uses_toyota_auto_hold_aeb(car_params) == legacy_hold
if legacy_hold:
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
else:
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
can_parsers = CarState.get_can_parsers(car_params)
car_state = CarState(car_params, SimpleNamespace(flags=0))
car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
assert "PRE_COLLISION_2" not in can_parsers[Bus.cam].vl
assert (0x344 in can_parsers[Bus.cam].vl) == legacy_hold
assert car_state.auto_brake_hold == legacy_hold
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H])
def test_auto_hold_is_disabled_by_default(self, candidate):
@pytest.mark.parametrize("candidate,hybrid", [
(CAR.TOYOTA_CAMRY_TSS2, False),
(CAR.TOYOTA_RAV4, False),
(CAR.TOYOTA_RAV4H, True),
(CAR.TOYOTA_RAV4_TSS2, False),
(CAR.TOYOTA_RAV4_TSS2, True),
(CAR.TOYOTA_RAV4_TSS2_2022, True),
])
def test_auto_hold_is_disabled_by_default(self, candidate, hybrid):
params = Params()
params.remove("ToyotaAutoHold")
car_params = CarInterface.get_params(
candidate,
{bus: {} for bus in range(8)},
[],
[CarParams.CarFw(ecu=Ecu.hybrid, address=0x7D2, fwVersion=b"test")] if hybrid else [],
alpha_long=False,
is_release=False,
docs=False,
@@ -233,6 +294,8 @@ class TestToyotaInterfaces:
assert not car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
assert 0x344 not in CarState.get_can_parsers(car_params)[Bus.cam].vl
def test_prius_openpilot_long_uses_hybrid_long_defaults(self):
car_params = CarInterface.get_params(
@@ -735,14 +798,43 @@ class TestToyotaFingerprint:
class TestToyotaCarController:
def test_corolla_tss2_hands_off_immediately_when_driver_is_steering(self):
assert not get_toyota_lat_active(CAR.TOYOTA_COROLLA_TSS2, True, 117, True)
@pytest.mark.parametrize("driver_torque", [-191, -117, -99, 99, 117, 191])
def test_toyota_assisting_driver_keeps_lateral_active(self, driver_torque):
assert get_toyota_lat_active(True, driver_torque)
def test_corolla_tss2_stays_active_without_driver_input(self):
assert get_toyota_lat_active(CAR.TOYOTA_COROLLA_TSS2, True, 99, False)
@pytest.mark.parametrize("driver_torque", [-MAX_USER_TORQUE, MAX_USER_TORQUE, MAX_USER_TORQUE + 1])
def test_toyota_high_driver_torque_still_disables_lateral(self, driver_torque):
assert not get_toyota_lat_active(True, driver_torque)
def test_toyota_driver_handoff_behavior_is_corolla_only(self):
assert get_toyota_lat_active(CAR.TOYOTA_RAV4_TSS2, True, 117, True)
def test_toyota_inactive_request_stays_inactive(self):
assert not get_toyota_lat_active(False, 0)
def test_toyota_assisting_driver_retains_rate_fault_protection(self):
counter = 0
requests = []
for _ in range(36):
counter, request = common_fault_avoidance(
150 >= MAX_STEER_RATE, get_toyota_lat_active(True, 117), counter, MAX_STEER_RATE_FRAMES,
)
requests.append(request)
assert requests == ([True] * 17 + [False]) * 2
@pytest.mark.parametrize("candidate", list(CAR))
def test_steer_rate_margin_is_corolla_only(self, candidate):
expected = 80 if candidate == CAR.TOYOTA_COROLLA_TSS2 else MAX_STEER_RATE
assert get_toyota_steer_rate_limit(candidate) == expected
@pytest.mark.parametrize("direction", [-1, 1])
def test_corolla_rate_margin_preserves_request_spacing(self, direction):
counter = 0
requests = []
for rate in [0] * 30 + [90 * direction] * 36 + [0] * 30:
counter, request = common_fault_avoidance(
abs(rate) >= get_toyota_steer_rate_limit(CAR.TOYOTA_COROLLA_TSS2),
get_toyota_lat_active(True, 117 * direction), counter, MAX_STEER_RATE_FRAMES,
)
requests.append(request)
assert requests == [True] * 30 + ([True] * 17 + [False]) * 2 + [True] * 30
@staticmethod
def _make_controller(*, standstill_req=False, last_standstill=False):
@@ -858,6 +950,31 @@ class TestToyotaCarController:
controller.update_auto_hold_state(cs, activation_frames=0)
assert not controller.brake_hold_active
def test_camry_auto_hold_uses_legacy_aeb_brake_path(self):
controller = self._make_controller()
controller.CP.carFingerprint = CAR.TOYOTA_CAMRY_TSS2
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
controller.frame = 0
cs = SimpleNamespace(
out=SimpleNamespace(
standstill=True,
cruiseState=SimpleNamespace(available=True, enabled=False),
gasPressed=False,
brakePressed=True,
gearShifter=structs.CarState.GearShifter.drive,
),
pre_collision_2={},
)
can_sends = controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("PRE_COLLISION_2", 0)], 0)
parser.update([(1, can_sends)])
assert controller.brake_hold_active
assert parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -1.0
assert parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 1
def test_prius_resume_request_releases_standstill_latch(self):
controller = self._make_controller(standstill_req=True, last_standstill=True)
@@ -1032,6 +1149,17 @@ class TestToyotaCarController:
assert gas_cmd == 0.12
def test_interceptor_does_not_apply_gas_during_auto_hold(self):
controller = self._make_controller()
controller.CP.enableGasInterceptorDEPRECATED = True
controller.accel = 1.5
controller.brake_hold_active = True
assert controller._compute_interceptor_gas_cmd(
SimpleNamespace(longActive=True),
SimpleNamespace(out=SimpleNamespace(standstill=True, vEgo=0.0)),
) == 0.0
def test_interceptor_non_stop_and_go_scales_with_accel_request(self):
controller = self._make_controller()
controller.CP.enableGasInterceptorDEPRECATED = True
@@ -1164,6 +1292,271 @@ class TestToyotaCarController:
assert CP.stopAccel == -1.5
class TestToyotaAutoHoldCruise:
@staticmethod
def _make_car(*, candidate=CAR.TOYOTA_RAV4_TSS2, hybrid=False, enabled=True, capability=True):
cp = CarInterface.get_non_essential_params(candidate)
cp.openpilotLongitudinalControl = True
cp.flags &= ~(ToyotaFlags.AUTO_BRAKE_HOLD.value | ToyotaFlags.HYBRID.value)
if capability:
cp.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value
if hybrid:
cp.flags |= ToyotaFlags.HYBRID.value
controller = CarController(DBC[candidate], cp)
cc = structs.CarControl(enabled=True, longActive=True)
cc.actuators.accel = -0.7
cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.stopping
cc.hudControl.leadDistanceBars = 3
cs = SimpleNamespace(
out=structs.CarState(
standstill=True,
gearShifter=structs.CarState.GearShifter.drive,
cruiseState=structs.CarState.CruiseState(available=True, enabled=True, standstill=True),
),
gvc=0.0,
acc_type=1,
pcm_acc_status=7,
pcm_follow_distance=1,
lkas_hud={},
pre_collision_2={},
)
toggles = SimpleNamespace(toyota_auto_hold=enabled, sng_hack=False, lock_doors=False, unlock_doors=False)
parser = CANParser(DBC[candidate][Bus.pt], [("ACC_CONTROL", 0), ("PRE_COLLISION_2", 0)], 0)
return SimpleNamespace(controller=controller, cc=cc, cs=cs, toggles=toggles, parser=parser)
@staticmethod
def _tick(car):
# Run through a complete ACC_CONTROL send interval and decode the actual
# controller output, including the ordinary standstill and PID paths.
car.can_sends = []
for _ in range(3):
now_nanos = car.controller.frame * 10_000_000
_, messages = car.controller.update(car.cc.as_reader(), car.cs, now_nanos, car.toggles)
car.can_sends.extend(messages)
car.parser.update([(now_nanos, messages)])
return car.parser.vl["ACC_CONTROL"]
@pytest.mark.parametrize("candidate,hybrid", [
(CAR.TOYOTA_RAV4, False),
(CAR.TOYOTA_RAV4H, True),
(CAR.TOYOTA_RAV4_TSS2, False),
(CAR.TOYOTA_RAV4_TSS2, True),
(CAR.TOYOTA_CAMRY_TSS2, False),
])
@pytest.mark.parametrize("sng_hack", [False, True])
def test_auto_hold_does_not_change_cruise_stop_and_go(self, candidate, hybrid, sng_hack):
car = self._make_car(candidate=candidate, hybrid=hybrid)
baseline = self._make_car(candidate=candidate, hybrid=hybrid, enabled=False)
car.toggles.sng_hack = baseline.toggles.sng_hack = sng_hack
for _ in range(50):
command = self._tick(car)
expected = self._tick(baseline)
assert not car.controller.brake_hold_active
assert dict(command) == dict(expected)
assert car.parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 0
for vehicle in (car, baseline):
vehicle.cc.actuators.accel = 1.5
vehicle.cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.starting
vehicle.cc.cruiseControl.resume = True
for _ in range(30):
command = self._tick(car)
expected = self._tick(baseline)
assert not car.controller.brake_hold_active
assert dict(command) == dict(expected)
assert not car.cs.out.gasPressed
assert command["ACCEL_CMD"] > 0
assert command["RELEASE_STANDSTILL"] == 1
def test_manual_stop_still_holds_with_only_aol_active(self):
car = self._make_car()
car.cc.enabled = False
car.cc.longActive = False
car.cc.latActive = True
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.brakePressed = True
# Include the first ACC_CONTROL transmission after the one-second latch.
for _ in range(35):
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
assert command["RELEASE_STANDSTILL"] == 0
car.cs.out.brakePressed = False
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
car.cs.out.gasPressed = True
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["ACCEL_CMD"] == 0.0
assert command["RELEASE_STANDSTILL"] == 1
car.cs.out.gasPressed = False
for _ in range(50):
self._tick(car)
assert not car.controller.brake_hold_active
car.cs.out.brakePressed = True
for _ in range(35):
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
@pytest.mark.parametrize("long_state", ["off", "pid", "starting", "stopping"])
@pytest.mark.parametrize("brake_pressed", [False, True])
def test_engaged_cruise_never_activates_auto_hold(self, long_state, brake_pressed):
car = self._make_car()
car.cc.actuators.longControlState = getattr(structs.CarControl.Actuators.LongControlState, long_state)
car.cs.out.brakePressed = brake_pressed
for _ in range(50):
self._tick(car)
assert not car.controller.brake_hold_active
@pytest.mark.parametrize("enabled,capability,candidate", [
(False, True, CAR.TOYOTA_RAV4_TSS2),
(True, False, CAR.TOYOTA_RAV4_TSS2),
])
def test_manual_hold_requires_toggle_and_capability(self, enabled, capability, candidate):
car = self._make_car(candidate=candidate, enabled=enabled, capability=capability)
car.cc.longActive = False
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.brakePressed = True
for _ in range(50):
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["ACCEL_CMD"] == 0.0
@pytest.mark.parametrize("release", ["toggle", "cancel", "main", "park", "reverse", "moving", "cruise"])
def test_manual_hold_releases_when_conditions_change(self, release):
car = self._make_car()
car.cc.longActive = False
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.brakePressed = True
for _ in range(35):
self._tick(car)
assert car.controller.brake_hold_active
if release == "toggle":
car.toggles.toyota_auto_hold = False
elif release == "cancel":
car.cc.cruiseControl.cancel = True
elif release == "main":
car.cs.out.cruiseState.available = False
elif release in ("park", "reverse"):
car.cs.out.gearShifter = getattr(structs.CarState.GearShifter, release)
elif release == "moving":
car.cs.out.standstill = False
else:
car.cs.out.cruiseState.enabled = True
car.cc.longActive = True
self._tick(car)
assert not car.controller.brake_hold_active
if release == "cruise":
car.cs.out.brakePressed = False
car.cc.actuators.accel = 1.5
car.cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.starting
car.cc.cruiseControl.resume = True
for _ in range(20):
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["ACCEL_CMD"] > 0
assert command["RELEASE_STANDSTILL"] == 1
class TestToyotaAutoHoldAeb:
_make_car = staticmethod(TestToyotaAutoHoldCruise._make_car)
_tick = staticmethod(TestToyotaAutoHoldCruise._tick)
@pytest.mark.parametrize("candidate", list(CAR))
@pytest.mark.parametrize("hybrid", [False, True])
def test_legacy_path_is_scoped_to_camry_and_early_rav4_hybrid(self, candidate, hybrid):
cp = SimpleNamespace(carFingerprint=candidate, flags=ToyotaFlags.HYBRID.value if hybrid else 0)
expected = candidate == CAR.TOYOTA_CAMRY_TSS2 or (candidate == CAR.TOYOTA_RAV4_TSS2 and hybrid)
assert uses_toyota_auto_hold_aeb(cp) == expected
@pytest.mark.parametrize("candidate,hybrid", [(CAR.TOYOTA_CAMRY_TSS2, False), (CAR.TOYOTA_RAV4_TSS2, True)])
def test_manual_stop_uses_legacy_hold_and_releases_on_gas(self, candidate, hybrid):
car = self._make_car(candidate=candidate, hybrid=hybrid)
car.cc.enabled = False
car.cc.longActive = False
car.cc.latActive = True
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.cruiseState.standstill = False
car.cs.out.brakePressed = True
car.cs.pre_collision_2 = {"DSS1GDRV": -0.1, "PBRTRGR": 0}
# Pass through the camera message until the pedal has been held at a stop.
command = self._tick(car)
assert not car.controller.brake_hold_active
assert car.parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -0.1
for _ in range(34):
command = self._tick(car)
assert car.controller.brake_hold_active
assert any(msg[0] == 0x344 for msg in car.can_sends)
assert car.parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -1.0
assert car.parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 1
# Do not simultaneously inject an ACC_CONTROL hold on legacy-path cars.
assert command["ACCEL_CMD"] == 0.0
assert command["RELEASE_STANDSTILL"] == 1
car.cs.out.brakePressed = False
for _ in range(50):
self._tick(car)
assert car.controller.brake_hold_active
assert car.parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -1.0
car.cs.out.gasPressed = True
self._tick(car)
assert not car.controller.brake_hold_active
assert car.parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -0.1
assert car.parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 0
@pytest.mark.parametrize("enabled,capability", [(False, True), (True, False)])
def test_rav4_hybrid_does_not_inject_legacy_messages_without_opt_in(self, enabled, capability):
car = self._make_car(hybrid=True, enabled=enabled, capability=capability)
car.cc.longActive = False
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.brakePressed = True
for _ in range(50):
self._tick(car)
assert not car.controller.brake_hold_active
assert all(msg[0] != 0x344 for msg in car.can_sends)
@pytest.mark.parametrize("release", ["toggle", "main", "park", "reverse", "moving", "cruise"])
def test_rav4_hybrid_legacy_hold_releases_when_conditions_change(self, release):
car = self._make_car(hybrid=True)
car.cc.longActive = False
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.brakePressed = True
for _ in range(35):
self._tick(car)
assert car.controller.brake_hold_active
if release == "toggle":
car.toggles.toyota_auto_hold = False
elif release == "main":
car.cs.out.cruiseState.available = False
elif release in ("park", "reverse"):
car.cs.out.gearShifter = getattr(structs.CarState.GearShifter, release)
elif release == "moving":
car.cs.out.standstill = False
else:
car.cs.out.cruiseState.enabled = True
self._tick(car)
assert not car.controller.brake_hold_active
class TestToyotaCarState:
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_PRIUS, CAR.TOYOTA_PRIUS_RETROFIT])
def test_legacy_prius_distance_button_generates_events(self, candidate):
@@ -89,6 +89,38 @@ def create_pcs_commands(packer, accel, active, mass):
return [msg1, msg2]
def create_brake_hold_command(packer, frame, pre_collision_2, brake_hold_active):
values = {s: pre_collision_2[s] for s in [
"DSS1GDRV",
"DS1STAT2",
"DS1STBK2",
"PCSWAR",
"PCSALM",
"PCSOPR",
"PCSABK",
"PBATRGR",
"PPTRGR",
"IBTRGR",
"CLEXTRGR",
"IRLT_REQ",
"BRKHLD",
"AVSTRGR",
"VGRSTRGR",
"PREFILL",
"PBRTRGR",
"PCSDIS",
"PBPREPMP",
] if s in pre_collision_2}
if brake_hold_active:
values = {
"DSS1GDRV": 0x3FF,
"PBRTRGR": frame % 730 < 727,
}
return packer.make_can_msg("PRE_COLLISION_2", 0, values)
def create_acc_cancel_command(packer):
values = {
"GAS_RELEASED": 0,
+10
View File
@@ -629,6 +629,16 @@ TOYOTA_AUTO_HOLD_CARS = (TSS2_CAR - RADAR_ACC_CAR - SECOC_CAR) | {
CAR.TOYOTA_RAV4H,
}
# The Camry uses the legacy camera AEB replacement for Auto Hold. The
# 2019-2021 RAV4 uses it only with the detected hybrid powertrain.
TOYOTA_AUTO_HOLD_AEB_CARS = {CAR.TOYOTA_CAMRY_TSS2}
def uses_toyota_auto_hold_aeb(CP: CarParams) -> bool:
return (CP.carFingerprint in TOYOTA_AUTO_HOLD_AEB_CARS or
(CP.carFingerprint == CAR.TOYOTA_RAV4_TSS2 and bool(CP.flags & ToyotaFlags.HYBRID.value)))
# no resume button press required
NO_STOP_TIMER_CAR = CAR.with_flags(ToyotaFlags.NO_STOP_TIMER)
@@ -3,6 +3,7 @@ from opendbc.can import CANParser
from opendbc.car import Bus, structs
from opendbc.car.interfaces import CarStateBase
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.gps import get_car_gps_config
from opendbc.car.volkswagen.values import DBC, CanBus, NetworkLocation, TransmissionType, GearShifter, \
CarControllerParams, VolkswagenFlags
@@ -24,6 +25,32 @@ class CarState(CarStateBase):
self.travel_assist_available = False
self.curvature_meas = 0.
self.klr_stock_values = {}
self.car_gps_config = get_car_gps_config(CP)
self.car_gps_supported = self.car_gps_config is not None
self.car_gps = None
self._car_gps_timestamp_nanos = 0
def _update_car_gps(self, cp) -> None:
if self.car_gps_config is None:
return
timestamps = [max(cp.ts_nanos[name].values(), default=0) for name in self.car_gps_config.messages]
if min(timestamps) > self._car_gps_timestamp_nanos and max(timestamps) - min(timestamps) <= 2_000_000_000:
gps = self.car_gps_config.decoder(*(cp.vl[name] for name in self.car_gps_config.messages))
if gps is not None and (self.car_gps is None or not gps["hasFix"] or
gps["unixTimestampMillis"] > self.car_gps["unixTimestampMillis"]):
timestamp_nanos = max(timestamps)
gps["timestamp_nanos"] = timestamp_nanos
self.car_gps = gps
self._car_gps_timestamp_nanos = timestamp_nanos
if self.car_gps is not None and cp._last_update_nanos - self._car_gps_timestamp_nanos > 2_500_000_000:
if self.car_gps["hasFix"]:
self.car_gps = {**self.car_gps, "hasFix": False, "speed": 0.0, "vNED": [0.0, 0.0, 0.0],
"bearingAccuracyDeg": 180.0, "timestamp_nanos": cp._last_update_nanos}
def get_car_gps(self):
return self.car_gps
def update_button_enable(self, buttonEvents: list[structs.CarState.ButtonEvent]):
if not self.CP.pcmCruise:
@@ -51,6 +78,7 @@ class CarState(CarStateBase):
pt_cp = can_parsers[Bus.pt]
cam_cp = can_parsers[Bus.cam]
ext_cp = pt_cp if self.CP.networkLocation == NetworkLocation.fwdCamera else cam_cp
self._update_car_gps(pt_cp)
if self.CP.flags & VolkswagenFlags.PQ:
return self.update_pq(pt_cp, cam_cp, ext_cp)
@@ -427,6 +455,9 @@ class CarState(CarStateBase):
# manually configure some optional and variable-rate/edge-triggered messages
pt_messages, cam_messages = [], []
gps_config = get_car_gps_config(CP)
if gps_config is not None:
pt_messages += [(name, 0) for name in gps_config.messages]
if not CP.flags & VolkswagenFlags.MLB:
pt_messages += [
@@ -0,0 +1,196 @@
import pytest
from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus
from opendbc.car.gps import VOLKSWAGEN_TAOS_GPS_MESSAGES, get_car_gps_config, parse_volkswagen_taos_can_gps
from opendbc.car.volkswagen.carstate import CarState
from opendbc.car.volkswagen.interface import CarInterface
from opendbc.car.volkswagen.values import CAR, DBC
# Synthetic frames, not route data: 40 N / 110 W, 12.5 m/s westbound,
# altitude 300 m, UTC 1,800,000,000, ten satellites, packet ID 2.
GPS_FRAMES = (
(0x36F, bytes.fromhex("02688909e09da31d"), 0),
(0x374, bytes.fromhex("02c0a83200000000"), 0),
(0x378, bytes.fromhex("0221415290010000"), 0),
(0x37B, bytes.fromhex("00d2496b7f4c0900"), 0),
)
@pytest.fixture
def gps_state():
cp = CarInterface.get_non_essential_params(CAR.VOLKSWAGEN_TAOS_MK1)
state = CarState(cp, None)
parsers = state.get_can_parsers(cp)
return state, parsers
def decode_frames(frames=GPS_FRAMES):
parser = CANParser("vw_mqb", [(name, 0) for name in VOLKSWAGEN_TAOS_GPS_MESSAGES], 0)
parser.update([(1_000_000_000, list(frames))])
return parser, [parser.vl[name] for name in VOLKSWAGEN_TAOS_GPS_MESSAGES]
def test_synthetic_gps_frame_decode():
parser, values = decode_frames()
gps = parse_volkswagen_taos_can_gps(*values)
assert parser.can_valid
assert gps["hasFix"]
assert gps["latitude"] == pytest.approx(40.0)
assert gps["longitude"] == pytest.approx(-110.0)
assert gps["speed"] == 12.5
assert gps["bearingDeg"] == 270.0
assert gps["altitude"] == 300.0
assert gps["unixTimestampMillis"] == 1_800_000_000_000
assert gps["satelliteCount"] == 10
assert gps["vNED"] == pytest.approx([0.0, -12.5, 0.0])
def test_unvalidated_hemisphere_is_not_a_fix():
frames = ((0x36F, bytes.fromhex("02688929e09da319"), 0), *GPS_FRAMES[1:])
_, values = decode_frames(frames)
gps = parse_volkswagen_taos_can_gps(*values)
assert not gps["hasFix"]
assert gps["vNED"] == [0.0, 0.0, 0.0]
@pytest.mark.parametrize("message,signal,value", (
(0, "GNSS_LatitudeMagnitude", 91.0),
(0, "GNSS_LongitudeMagnitude", 181.0),
(0, "GNSS_LatitudeMagnitude", float("nan")),
(0, "GNSS_PositionStatus", 0),
(0, "GNSS_PositionStatus", 1),
(0, "GNSS_PositionStatus", 2),
(3, "GNSS_Empfaenger_Status", 0),
(3, "GNSS_Genutzte_Satelliten", 0),
(3, "GNSS_Genutzte_Satelliten", 3),
))
def test_invalid_solution_is_not_a_fix(message, signal, value):
_, values = decode_frames()
values[message][signal] = value
gps = parse_volkswagen_taos_can_gps(*values)
assert not gps["hasFix"]
assert gps["speed"] == 0.0
assert gps["vNED"] == [0.0, 0.0, 0.0]
def test_no_constellation_or_null_position_is_not_a_fix():
_, values = decode_frames()
values[3]["GNSS_GPS_in_Nutzung"] = values[3]["GNSS_GLONASS_in_Nutzung"] = 0
assert not parse_volkswagen_taos_can_gps(*values)["hasFix"]
values[3]["GNSS_GPS_in_Nutzung"] = 1
values[0]["GNSS_LatitudeMagnitude"] = values[0]["GNSS_LongitudeMagnitude"] = 0
assert not parse_volkswagen_taos_can_gps(*values)["hasFix"]
def test_initial_clock_and_mixed_epochs_are_rejected():
_, values = decode_frames()
values[3]["GNSS_UTC_Zeit"] = 0
assert parse_volkswagen_taos_can_gps(*values) is None
values[3]["GNSS_UTC_Zeit"] = 1_800_000_000
values[1]["GNSS_Nachrichtenpaket_ID2"] = 1
assert parse_volkswagen_taos_can_gps(*values) is None
def test_invalid_motion_and_altitude_have_unknown_accuracy():
_, values = decode_frames()
values[1]["GNSS_Speed"] = 127.75
values[1]["GNSS_Bearing"] = 409.5
values[2]["GNSS_Ortung_Hoehe"] = 7690.0
gps = parse_volkswagen_taos_can_gps(*values)
assert gps["hasFix"]
assert gps["speed"] == gps["bearingDeg"] == gps["altitude"] == 0.0
assert gps["bearingAccuracyDeg"] == 180.0
assert gps["speedAccuracy"] == 100.0
assert gps["verticalAccuracy"] == 500.0
assert gps["vNED"] == [0.0, 0.0, 0.0]
def test_unknown_course_does_not_report_accurate_zero_velocity():
_, values = decode_frames()
values[1]["GNSS_Bearing"] = 409.5
gps = parse_volkswagen_taos_can_gps(*values)
assert gps["hasFix"]
assert gps["speed"] == 12.5
assert gps["speedAccuracy"] == 100.0
assert gps["vNED"] == [0.0, 0.0, 0.0]
def test_gps_is_only_enabled_for_the_taos():
for model in (CAR.VOLKSWAGEN_TAOS_MK1, CAR.VOLKSWAGEN_GOLF_MK7, CAR.VOLKSWAGEN_ID4_MK1):
cp = CarInterface.get_non_essential_params(model)
state = CarState(cp, None)
config = get_car_gps_config(cp)
expected = model == CAR.VOLKSWAGEN_TAOS_MK1
assert state.car_gps_supported is expected
assert (config is not None) is expected
if expected:
assert config.messages == VOLKSWAGEN_TAOS_GPS_MESSAGES
cp.brand = "mock"
assert get_car_gps_config(cp) is None
def test_carstate_update_reads_gps(gps_state):
state, parsers = gps_state
parsers[Bus.pt].update([(1_000_000_000, list(GPS_FRAMES))])
state.update(parsers, None)
gps = state.get_car_gps()
assert gps["hasFix"]
assert gps["timestamp_nanos"] == 1_000_000_000
assert gps["longitude"] == pytest.approx(-110.0)
def test_partial_startup_and_missing_gps_are_optional(gps_state):
state, parsers = gps_state
parser = parsers[Bus.pt]
packer = CANPacker(DBC[state.CP.carFingerprint][Bus.pt])
blink = packer.make_can_msg("Blinkmodi_02", parser.bus, {})
for time in (1_000_000_000, 2_000_000_000):
parser.update([(time, [blink])])
state._update_car_gps(parser)
assert state.get_car_gps() is None
assert parser.can_valid
parser.update([(3_000_000_000, list(GPS_FRAMES[:3]))])
state._update_car_gps(parser)
assert state.get_car_gps() is None
assert all(parser.message_states[address].ignore_alive for address, _, _ in GPS_FRAMES)
@pytest.mark.parametrize("dropout", ("missing_position", "frozen_clock", "missing_all"))
def test_gps_dropout_and_recovery(gps_state, dropout):
state, parsers = gps_state
parser = parsers[Bus.pt]
parser.update([(1_000_000_000, list(GPS_FRAMES))])
state._update_car_gps(parser)
assert state.get_car_gps()["hasFix"]
frames = list(GPS_FRAMES)
if dropout == "missing_position":
frames = frames[1:]
elif dropout == "missing_all":
frames = []
parser.update([(4_000_000_000, frames)])
state._update_car_gps(parser)
assert not state.get_car_gps()["hasFix"]
assert state.get_car_gps()["timestamp_nanos"] == 4_000_000_000
assert state.get_car_gps()["speed"] == 0.0
packer = CANPacker("vw_mqb")
status = {**parser.vl["GNSS_05"], "GNSS_UTC_Zeit": 1_800_000_004}
parser.update([(5_000_000_000, [*GPS_FRAMES[:3], packer.make_can_msg("GNSS_05", parser.bus, status)])])
state._update_car_gps(parser)
assert state.get_car_gps()["hasFix"]
assert state.get_car_gps()["unixTimestampMillis"] == 1_800_000_004_000
def test_invalid_receiver_status_clears_existing_fix(gps_state):
state, parsers = gps_state
parser = parsers[Bus.pt]
parser.update([(1_000_000_000, list(GPS_FRAMES))])
state._update_car_gps(parser)
status = {**parser.vl["GNSS_05"], "GNSS_Empfaenger_Status": 0}
packer = CANPacker("vw_mqb")
parser.update([(2_000_000_000, [*GPS_FRAMES[:3], packer.make_can_msg("GNSS_05", parser.bus, status)])])
state._update_car_gps(parser)
assert not state.get_car_gps()["hasFix"]
@@ -170,8 +170,6 @@ class CarController(CarControllerBase):
# convention = driver pushing right → yields right authority
# (LOOSELY/+ arm), retains left (INV/- arm).
# Yield arm scales with |drv| above OVERRIDE_THRESH — strong presses
# (potholes, hard corrections) cross past zero so EPS hands the wheel
# to the driver in their direction.
excess = max(0.0, self.lca_auth_drv_mag_filt - float(P.LCA_AUTH_OVERRIDE_ENTER))
yield_signed = float(P.LCA_AUTH_YIELD_BASE) - P.LCA_AUTH_YIELD_SLOPE * excess
yield_signed = max(float(P.LCA_AUTH_YIELD_MIN), min(yield_signed, float(P.LCA_AUTH_YIELD_BASE)))
@@ -250,12 +248,12 @@ class CarController(CarControllerBase):
# LCA_5 (formerly SPEED_1) - 0x67 - 50 Hz
# Contains wheel speeds + LCA signals (LCA_TURN_BITS, LCA_5_STEER)
if self.frame % 2 == 0: # 50 Hz
# Initialize counter from CarState on first run
if self.lca_5_counter is None:
if not lat_active or self.lca_5_counter is None:
self.lca_5_counter = CS.msg_lca_5['COUNTER']
# Increment counter by +4, wrap at 15 (0xF never used)
self.lca_5_counter = (self.lca_5_counter + 4) % 15
if lat_active:
self.lca_5_counter = (self.lca_5_counter + 4) % 15
can_sends.append(create_lca_5_message(self.packer, lat_active, apply_angle,
CS.msg_lca_5, self.lca_5_counter))
@@ -1,11 +1,15 @@
from collections import defaultdict
from types import SimpleNamespace
import pytest
from opendbc.can.parser import CANParser
from opendbc.car.volvo.carcontroller import CarController
from opendbc.car.volvo.helpers import checksum_lca_5_message
from opendbc.car.volvo.interface import CarInterface
from opendbc.car.volvo.values import CAR, DBC
from opendbc.car.volvo.volvocan import create_c1_checksum
from opendbc.safety.tests.libsafety import libsafety_py
def _zero_message():
@@ -53,21 +57,65 @@ def test_controller_emits_valid_eight_byte_messages_and_lca5_checksum():
assert data[2] == checksum_lca_5_message(data[0], data[1], data[3], data[4], data[5])
def test_controller_relays_stock_lca5_angle_when_inactive():
cp = CarInterface.get_non_essential_params("VOLVO_XC40_RECHARGE")
@pytest.mark.parametrize("fingerprint", [CAR.POLESTAR_2, CAR.VOLVO_XC40_RECHARGE])
def test_controller_relays_complete_stock_lca5_when_inactive(fingerprint):
cp = CarInterface.get_non_essential_params(fingerprint)
controller = CarController(DBC[cp.carFingerprint], cp)
parser = CANParser("volvo_mid_1", [("LCA_5", 50)], 0)
stock_data = bytes.fromhex("88c04cef1190ba00")
parser.update([0, [(0x67, stock_data, 0)]])
cs = _state()
cs.msg_lca_5["LCA_5_STEER"] = 12.0
cs.msg_lca_5 = parser.vl["LCA_5"]
cc = SimpleNamespace(latActive=False, actuators=_Actuators())
safety = libsafety_py.libsafety
config = cp.safetyConfigs[0]
assert safety.set_safety_hooks(config.safetyModel.raw, config.safetyParam) == 0
safety.init_tests()
safety.set_controls_allowed(False)
safety.safety_rx_hook(libsafety_py.make_CANPacket(0x67, 0, stock_data))
_, can_sends = controller.update(cc, cs, 0, None)
lca5 = next(msg for msg in can_sends if msg[0] == 0x67)
assert lca5[1] == stock_data
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(lca5[0], lca5[2], lca5[1]))
# The inactive path must not manufacture a new angle command.
raw = ((lca5[1][6] & 0x7F) << 8) | lca5[1][7]
if raw & (1 << 14):
raw -= 1 << 15
assert abs(raw * 0.05596 - 12.0) < 0.1
cs.msg_lca_5["COUNTER"] = 7
controller.update(cc, cs, 0, None)
controller.update(cc, cs, 0, None)
controller.update(cc, cs, 0, None)
cc.latActive = True
_, can_sends = controller.update(cc, cs, 0, None)
active_lca5 = next(msg for msg in can_sends if msg[0] == 0x67)
assert active_lca5[1][3] >> 4 == 11
@pytest.mark.parametrize("fingerprint", [CAR.POLESTAR_2, CAR.VOLVO_XC40_RECHARGE])
@pytest.mark.parametrize("driver_torque", [20.0, -20.0, 128.0, -127.0])
def test_override_lca_stream_passes_safety_and_recovers(fingerprint, driver_torque):
cp = CarInterface.get_non_essential_params(fingerprint)
controller = CarController(DBC[cp.carFingerprint], cp)
cs = _state()
cc = SimpleNamespace(latActive=True, actuators=_Actuators())
safety = libsafety_py.libsafety
config = cp.safetyConfigs[0]
assert safety.set_safety_hooks(config.safetyModel.raw, config.safetyParam) == 0
safety.init_tests()
safety.set_controls_allowed(True)
for active, torque in [(True, 0), (True, driver_torque), (True, -driver_torque),
(True, 0), (False, 0), (True, 0)]:
cc.latActive = active
cs.out.steeringTorque = torque
for _ in range(350):
_, messages = controller.update(cc, cs, 0, None)
address, data, bus = next(msg for msg in messages if msg[0] == 0x58)
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)), (
active, torque, controller.frame, controller.lca_auth_pos, controller.lca_auth_neg)
if active and torque == 0:
assert controller.lca_auth_pos == 614
assert controller.lca_auth_neg == -614
elif active:
assert min(abs(controller.lca_auth_pos), abs(controller.lca_auth_neg)) == 0
def _c1_state():
+1 -3
View File
@@ -71,14 +71,12 @@ class CarControllerParams:
# (potholes, lane corrections) get full yield while light sustained pressure
# only gets a soft yield. yield_signed = YIELD_BASE − YIELD_SLOPE *
# max(0, drv_mag_filt − OVERRIDE_ENTER), clamped to [YIELD_MIN, YIELD_BASE].
# At |drv|=7 (just over threshold): yield = +60 (light resistance).
# At |drv|=14: yield ≈ -4 (crosses past zero — EPS hands wheel to driver).
# drv_mag_filt is a low-pass of |drv| (alpha=0.04, ~250 ms time constant) —
# without it, 1-2 unit driver-torque jitter became ~10 unit yield-arm jitter
# which PSCM converted to felt ripple at sustained co-steering pressure.
LCA_AUTH_YIELD_BASE = 60 # yield-arm magnitude at the override threshold
LCA_AUTH_YIELD_SLOPE = 8 # counts of yield reduction per unit |drv torque| above threshold
LCA_AUTH_YIELD_MIN = -30 # cap how far past zero the yield arm can go (full hand-over)
LCA_AUTH_YIELD_MIN = 0 # yield authority without crossing the safety sign boundary
LCA_AUTH_YIELD_LP_ALPHA = 0.04 # LP-filter coefficient on |drv| for yield calc (~250 ms tau)
LCA_AUTH_SPLIT = 200 # symmetric → asymmetric handover
LCA_AUTH_REBUILD_RATE = 230 # counts/s (≈ 2.7 s rebuild from 0 to 614)
+4 -1
View File
@@ -250,6 +250,9 @@ def create_lca_5_message(packer, lat_active: bool, target_angle_deg: float, msg_
CAN message for LCA_5 on bus 2
"""
if not lat_active:
return packer.make_can_msg('LCA_5', 2, msg_lca_5)
# DBC defines LCA_5_STEER as 15-bit signed with scale 0.05596 deg/count
# Packer handles the encoding automatically - just pass the angle in degrees
@@ -261,7 +264,7 @@ def create_lca_5_message(packer, lat_active: bool, target_angle_deg: float, msg_
'WHEEL_SPEED_2': msg_lca_5['WHEEL_SPEED_2'],
'NEW_SIGNAL_5': msg_lca_5['NEW_SIGNAL_5'],
'NEW_SIGNAL_2': msg_lca_5['NEW_SIGNAL_2'],
'LCA_5_STEER': target_angle_deg if lat_active else msg_lca_5['LCA_5_STEER'],
'LCA_5_STEER': target_angle_deg,
'COUNTER': counter,
}
@@ -727,10 +727,14 @@ BO_ 1259 LOCAL_TIME2: 8 XXX
SG_ NEW_SIGNAL_3 : 39|1@0+ (1,0) [0|1] "" XXX
BO_ 1264 LOCAL_TIME: 8 XXX
SG_ HOURS : 12|5@0+ (1,0) [0|31] "" XXX
SG_ MINUTES : 21|6@0+ (1,0) [0|63] "" XXX
SG_ SECONDS : 31|8@0+ (1,0) [0|59] "" XXX
SG_ HOURS : 8|8@1+ (1,0) [0|23] "" XXX
SG_ MINUTES : 16|8@1+ (1,0) [0|59] "" XXX
SG_ SECONDS : 24|8@1+ (1,0) [0|59] "" XXX
SG_ MONTH : 34|4@1+ (1,0) [1|12] "" XXX
SG_ YEAR : 40|8@1+ (1,2000) [2000|2255] "" XXX
SG_ DAY : 48|8@1+ (1,0) [1|31] "" XXX
CM_ BO_ 1264 "Cluster wall clock, 1Hz. Local time, not UTC. All 0xFF until the cluster initializes.";
CM_ SG_ 96 BRAKE_PRESSURE "User applied brake pedal pressure. Ramps from computer applied pressure on falling edge of cruise. Cruise cancels if !=0";
CM_ SG_ 101 BRAKE_POSITION "User applied brake pedal position, max is ~700. Signed on some vehicles";
CM_ SG_ 203 ADAS_ActvACISta "ADAS Active AngleControlInterface State";
@@ -1,5 +1,15 @@
CM_ "IMPORT _subaru_global.dbc";
BO_ 1723 AVH_Request: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ REQUEST : 16|2@1+ (1,0) [0|3] "" XXX
BO_ 811 AVH_Status: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ ENABLED : 45|1@1+ (1,0) [0|1] "" XXX
BO_ 72 Transmission: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
@@ -964,10 +964,14 @@ BO_ 1259 LOCAL_TIME2: 8 XXX
SG_ NEW_SIGNAL_3 : 39|1@0+ (1,0) [0|1] "" XXX
BO_ 1264 LOCAL_TIME: 8 XXX
SG_ HOURS : 12|5@0+ (1,0) [0|31] "" XXX
SG_ MINUTES : 21|6@0+ (1,0) [0|63] "" XXX
SG_ SECONDS : 31|8@0+ (1,0) [0|59] "" XXX
SG_ HOURS : 8|8@1+ (1,0) [0|23] "" XXX
SG_ MINUTES : 16|8@1+ (1,0) [0|59] "" XXX
SG_ SECONDS : 24|8@1+ (1,0) [0|59] "" XXX
SG_ MONTH : 34|4@1+ (1,0) [1|12] "" XXX
SG_ YEAR : 40|8@1+ (1,2000) [2000|2255] "" XXX
SG_ DAY : 48|8@1+ (1,0) [1|31] "" XXX
CM_ BO_ 1264 "Cluster wall clock, 1Hz. Local time, not UTC. All 0xFF until the cluster initializes.";
CM_ SG_ 96 BRAKE_PRESSURE "User applied brake pedal pressure. Ramps from computer applied pressure on falling edge of cruise. Cruise cancels if !=0";
CM_ SG_ 101 BRAKE_POSITION "User applied brake pedal position, max is ~700. Signed on some vehicles";
CM_ SG_ 203 ADAS_ActvACISta "ADAS Active AngleControlInterface State";
@@ -307,6 +307,16 @@ VAL_ 544 AEB_Status 12 "AEB related" 8 "AEB actuation" 4 "AEB related" 0 "No AEB
CM_ "subaru_global_2017.dbc starts here";
BO_ 1723 AVH_Request: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ REQUEST : 16|2@1+ (1,0) [0|3] "" XXX
BO_ 811 AVH_Status: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ ENABLED : 45|1@1+ (1,0) [0|1] "" XXX
BO_ 72 Transmission: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
@@ -236,6 +236,9 @@ BO_ 1013 ID3F5VCFRONT_lighting: 8 VEH
SG_ VCFRONT_indicatorRightRequest : 2|2@1+ (1,0) [0|2] "" Receiver
SG_ VCFRONT_indicatorLeftRequest : 0|2@1+ (1,0) [0|2] "" Receiver
BO_ 991 UI_status2: 8 VehicleBus
SG_ UI_activeTouchPoints : 24|8@1+ (1,0) [0|255] "" VehicleBus
VAL_ 568 SpdCtrlLvr_Stat 32 "DN_1ST" 16 "UP_1ST" 8 "DN_2ND" 4 "UP_2ND" 2 "RWD" 1 "FWD" 0 "IDLE" ;
VAL_ 568 DTR_Dist_Rq 255 "SNA" 200 "ACC_DIST_7" 166 "ACC_DIST_6" 133 "ACC_DIST_5" 100 "ACC_DIST_4" 66 "ACC_DIST_3" 33 "ACC_DIST_2" 0 "ACC_DIST_1" ;
VAL_ 568 TurnIndLvr_Stat 3 "SNA" 2 "RIGHT" 1 "LEFT" 0 "IDLE" ;
+30
View File
@@ -1610,6 +1610,36 @@ BO_ 391 Motor_EV_01: 8 Motor_MQB_BEV
SG_ MO_HVEM_Eskalation : 54|1@1+ (1.0,0.0) [0.0|1] "" XXX
SG_ MO_HVEM_MaxLeistung : 55|9@1+ (50,0) [0|25450] "Unit_Watt" XXX
BO_ 879 GNSS_01: 8 XXX
SG_ GNSS_Nachrichtenpaket_ID1 : 0|2@1+ (1,0) [0|3] "" XXX
SG_ GNSS_LatitudeMagnitude : 2|27@1+ (0.000001,0) [0|90] "deg" XXX
SG_ GNSS_LatitudeSouth : 29|1@1+ (1,0) [0|1] "" XXX
SG_ GNSS_LongitudeMagnitude : 30|28@1+ (0.000001,0) [0|180] "deg" XXX
SG_ GNSS_LongitudeWest : 58|1@1+ (1,0) [0|1] "" XXX
SG_ GNSS_PositionStatus : 59|2@1+ (1,0) [0|3] "" XXX
BO_ 884 GNSS_02: 8 XXX
SG_ GNSS_Nachrichtenpaket_ID2 : 0|2@1+ (1,0) [0|3] "" XXX
SG_ GNSS_Bearing : 12|12@1+ (0.1,0) [0|359.9] "deg" XXX
SG_ GNSS_Speed : 24|9@1+ (0.25,0) [0|127.5] "m/s" XXX
BO_ 888 GNSS_04: 8 XXX
SG_ GNSS_Nachrichtenpaket_ID4 : 0|2@1+ (1,0) [0|3] "" XXX
SG_ GNSS_Ortung_Zeit_in_GPSWoche : 2|30@1+ (1,0) [0|604800001] "ms" XXX
SG_ GNSS_Ortung_Hoehe : 32|12@1+ (2,-500) [-500|7686] "m" XXX
BO_ 891 GNSS_05: 8 XXX
SG_ GNSS_UTC_Zeit : 0|32@1+ (1,0) [1|4294967295] "s" XXX
SG_ GNSS_Empfaenger_Status : 32|1@1+ (1,0) [0|1] "" XXX
SG_ GNSS_GPS_in_Nutzung : 33|1@1+ (1,0) [0|1] "" XXX
SG_ GNSS_GLONASS_in_Nutzung : 34|1@1+ (1,0) [0|1] "" XXX
SG_ GNSS_Empfangbare_Satelliten : 35|5@1+ (1,0) [0|31] "" XXX
SG_ GNSS_Sichtbare_Satelliten : 40|5@1+ (1,0) [0|31] "" XXX
SG_ GNSS_Genutzte_Satelliten : 45|5@1+ (1,0) [0|31] "" XXX
SG_ GNSS_Nachrichtenpaket_ID5 : 50|2@1+ (1,0) [0|3] "" XXX
CM_ BO_ 879 "Taos coordinate layout decoded from CAN/GPS comparison. Only north/west coordinates and position status 3 have been validated.";
CM_ BO_ 884 "Taos course and ground speed decoded from CAN/GPS comparison. Remaining fields are not decoded.";
CM_ SG_ 134 LWI_Lenkradwinkel "Steering angle WITH variable ratio effect included";
CM_ SG_ 159 EPS_HCA_Status "Status of Heading Control Assist feature";
CM_ SG_ 159 EPS_Lenkmoment "Steering input by driver, torque";
+24 -6
View File
@@ -96,6 +96,8 @@ static bool ford_lka_steering = false;
static bool ford_extended_lateral = false;
static bool ford_longitudinal = false;
static bool ford_cancel_resume_button = false;
static bool ford_mach_e_curvature = false;
static int ford_path_angle_last = 0;
// Curvature rate limits
#define FORD_LIMITS(limit_lateral_acceleration) { \
@@ -121,10 +123,10 @@ static bool ford_cancel_resume_button = false;
static const AngleSteeringLimits FORD_STEERING_LIMITS = FORD_LIMITS(false);
#define FORD_EXTENDED_LIMITS(limit_lateral_acceleration) { \
#define FORD_EXTENDED_LIMITS(limit_lateral_acceleration, max_curvature_error) { \
.max_angle = 1000, \
.angle_deg_to_can = 50000, \
.max_angle_error = 100, \
.max_angle_error = (max_curvature_error), \
.angle_rate_up_lookup = { \
{5., 16., 25.}, \
{0.0025, 0.0014, 0.00018} \
@@ -140,7 +142,7 @@ static const AngleSteeringLimits FORD_STEERING_LIMITS = FORD_LIMITS(false);
.inactive_angle_is_zero = true, \
}
static const AngleSteeringLimits FORD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(false);
static const AngleSteeringLimits FORD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(false, 100);
static void ford_rx_hook(const CANPacket_t *msg) {
if (msg->bus == FORD_MAIN_BUS) {
@@ -318,7 +320,8 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
// Safety check for LateralMotionControl2 action
if (msg->addr == FORD_LateralMotionControl2) {
static const AngleSteeringLimits FORD_CANFD_STEERING_LIMITS = FORD_LIMITS(true);
static const AngleSteeringLimits FORD_CANFD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(true);
static const AngleSteeringLimits FORD_CANFD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(true, 100);
static const AngleSteeringLimits FORD_MACH_E_CURVATURE_LIMITS = FORD_EXTENDED_LIMITS(true, 300);
// Signal: LatCtl_D2_Rq
bool steer_control_enabled = ((msg->data[0] >> 4) & 0x7U) != 0U;
@@ -336,9 +339,19 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
if (ford_extended_lateral) {
violation |= desired_path_offset != 0;
violation |= (desired_curvature_rate < -1024) || (desired_curvature_rate > 1023);
violation |= desired_path_angle != 0;
if (desired_path_angle != 0) {
const float speed = vehicle_speed.max / VEHICLE_SPEED_FACTOR;
const float curvature = (float)SAFETY_ABS(desired_curvature) / 50000.0f;
const float path_angle = (float)SAFETY_ABS(desired_path_angle) / 2000.0f;
const float combined_acceleration = (curvature + path_angle / SAFETY_MAX(speed, 1.0f)) * speed * speed;
violation |= !ford_mach_e_curvature || !steer_control_enabled || !controls_allowed;
violation |= (speed < 3.0f) || (speed >= 8.8f);
violation |= (SAFETY_ABS(desired_curvature) < 975) || (SAFETY_ABS(desired_path_angle) > 320);
violation |= (desired_curvature * desired_path_angle <= 0) || (combined_acceleration > 2.5f);
violation |= SAFETY_ABS(desired_path_angle - ford_path_angle_last) > 110;
}
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled,
FORD_CANFD_EXTENDED_STEERING_LIMITS);
ford_mach_e_curvature ? FORD_MACH_E_CURVATURE_LIMITS : FORD_CANFD_EXTENDED_STEERING_LIMITS);
if (!steer_control_enabled) {
violation |= (desired_curvature != 0) || (desired_curvature_rate != 0);
}
@@ -352,6 +365,8 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
if (violation) {
tx = false;
} else {
ford_path_angle_last = desired_path_angle;
}
}
@@ -406,8 +421,11 @@ static safety_config ford_init(uint16_t param) {
const uint16_t FORD_PARAM_CANFD = 2;
const uint16_t FORD_PARAM_LKA_STEERING = 4;
const uint16_t FORD_PARAM_MACH_E_CURVATURE = 8;
const bool ford_canfd = GET_FLAG(param, FORD_PARAM_CANFD);
ford_lka_steering = GET_FLAG(param, FORD_PARAM_LKA_STEERING);
ford_mach_e_curvature = ford_canfd && GET_FLAG(param, FORD_PARAM_MACH_E_CURVATURE);
ford_path_angle_last = 0;
ford_extended_lateral = false;
ford_cancel_resume_button = false;
+1 -1
View File
@@ -330,7 +330,7 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
const int expected_track2 = 497 + (2 * ((int)track1 - 264));
if ((msg->data[4] & 0x70U) != 0U ||
(msg->data[5] != hyundai_ray_pedal_checksum(msg)) ||
(enabled && (track1 < 264U || track1 > 397U || track2 < 497U || track2 > 766U ||
(enabled && (track1 < 264U || track1 > 473U || track2 < 497U || track2 > 919U ||
SAFETY_ABS((int)track2 - expected_track2) > 40)) ||
(!enabled && ((track1 != 0U) || (track2 != 0U))) ||
longitudinal_interceptor_checks(msg) ||
@@ -65,6 +65,7 @@
static bool hyundai_canfd_alt_buttons = false;
static bool hyundai_canfd_lka_steering_alt = false;
static bool hyundai_canfd_angle_steering = false;
static bool hyundai_canfd_no_stock_lka = false;
static bool hyundai_ccnc = false;
static bool hyundai_canfd_ccnc_angle_long = false;
static bool hyundai_canfd_lka_alt_drive_gear = false;
@@ -100,7 +101,8 @@ static bool hyundai_canfd_lka_alt_openpilot_allowed(void) {
}
static bool hyundai_canfd_lka_alt_stock_forwarding(void) {
return hyundai_canfd_lka_steering_alt && hyundai_canfd_angle_steering && !hyundai_canfd_lka_alt_openpilot_allowed();
return hyundai_canfd_lka_steering_alt && hyundai_canfd_angle_steering &&
!hyundai_canfd_no_stock_lka && !hyundai_canfd_lka_alt_openpilot_allowed();
}
static void hyundai_canfd_rx_all_hook(const CANPacket_t *msg) {
@@ -270,6 +272,10 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) {
const int lkas_angle_active = (msg->data[9] >> 4U) & 0x3U;
const bool steer_angle_req = lkas_angle_active != 1;
if (hyundai_canfd_no_stock_lka && steer_angle_req && !hyundai_canfd_lka_alt_openpilot_allowed()) {
tx = false;
}
int desired_angle = (msg->data[11] << 6U) | (msg->data[10] >> 2U);
desired_angle = to_signed(desired_angle, 14);
@@ -364,6 +370,7 @@ static safety_config hyundai_canfd_init(uint16_t param) {
const uint16_t HYUNDAI_PARAM_CANFD_LKA_STEERING_ALT = 128;
const uint16_t HYUNDAI_PARAM_CANFD_ALT_BUTTONS = 32;
const uint16_t HYUNDAI_PARAM_CANFD_ANGLE_STEERING = 1024;
const uint16_t HYUNDAI_PARAM_CANFD_NO_STOCK_LKA = 4096U;
const uint16_t HYUNDAI_PARAM_CCNC = 32768U;
static const CanMsg HYUNDAI_CANFD_LKA_STEERING_TX_MSGS[] = {
@@ -479,12 +486,15 @@ static safety_config hyundai_canfd_init(uint16_t param) {
{0x7C4, 2, 8, .check_relay = true}, /* camera support frame */ \
{0xEA, 2, 24, .check_relay = true}, /* MDPS support frame */ \
hyundai_common_init(param);
// This CAN-FD-only bit is independent of classic CAN's NON_SCC mode.
hyundai_common_init(param & ~HYUNDAI_PARAM_CANFD_NO_STOCK_LKA);
gen_crc_lookup_table_16(0x1021, hyundai_canfd_crc_lut);
hyundai_canfd_alt_buttons = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ALT_BUTTONS);
hyundai_canfd_lka_steering_alt = GET_FLAG(param, HYUNDAI_PARAM_CANFD_LKA_STEERING_ALT);
hyundai_canfd_angle_steering = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE_STEERING);
hyundai_canfd_no_stock_lka = hyundai_canfd_angle_steering && hyundai_canfd_lka_steering &&
hyundai_canfd_lka_steering_alt && GET_FLAG(param, HYUNDAI_PARAM_CANFD_NO_STOCK_LKA);
hyundai_ccnc = GET_FLAG(param, HYUNDAI_PARAM_CCNC);
hyundai_canfd_ccnc_angle_long = hyundai_longitudinal && hyundai_canfd_lka_steering &&
hyundai_canfd_lka_steering_alt && hyundai_canfd_angle_steering && hyundai_ccnc;
@@ -136,6 +136,8 @@ static uint32_t subaru_compute_checksum(const CANPacket_t *msg) {
return checksum;
}
#include "opendbc/safety/modes/subaru_avh.h"
static void subaru_rx_hook(const CANPacket_t *msg) {
const unsigned int alt_main_bus = subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_MAIN_BUS;
const unsigned int status_bus = subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_CAM_BUS;
@@ -308,6 +310,10 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (msg->addr == 0x6BBU) {
violation |= !subaru_avh_tx(msg);
}
if (violation){
tx = false;
}
@@ -315,6 +321,12 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
}
static safety_config subaru_init(uint16_t param) {
static const CanMsg SUBARU_LEGACY_AVH_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
{0x6BBU, SUBARU_ALT_BUS, 8, .check_relay = false},
};
static const CanMsg SUBARU_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_MAIN_BUS, MSG_SUBARU_ES_LKAS)
SUBARU_COMMON_TX_MSGS(SUBARU_MAIN_BUS)
@@ -455,12 +467,22 @@ static safety_config subaru_init(uint16_t param) {
subaru_stop_and_go ? BUILD_SAFETY_CFG(subaru_rx_checks, SUBARU_STOP_AND_GO_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_rx_checks, SUBARU_TX_MSGS);
}
bool avh_enabled = false;
#ifdef ALLOW_DEBUG
avh_enabled = GET_FLAG(param, 1024U) && subaru_gen2 && subaru_lkas_angle && subaru_fixed_angle_limits &&
subaru_stop_start_button && !subaru_d_platform && !GET_FLAG(param, 2U) && !subaru_redneck_cruise;
#endif
subaru_avh_init(avh_enabled);
if (avh_enabled) {
ret = BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_LEGACY_AVH_TX_MSGS);
}
return ret;
}
const safety_hooks subaru_hooks = {
.init = subaru_init,
.rx = subaru_rx_hook,
.rx_all = subaru_avh_rx,
.tx = subaru_tx_hook,
.get_counter = subaru_get_counter,
.get_checksum = subaru_get_checksum,
@@ -0,0 +1,140 @@
#pragma once
// Legacy startup AVH only. Never transmit the 0x32B status message.
static const unsigned int SUBARU_AVH_INPUTS[] = {0x6BBU, 0x32BU, 0x40U, 0x48U, 0x13AU, 0x174U};
static uint8_t subaru_avh_data[6][8];
static uint32_t subaru_avh_ts[6];
static bool subaru_avh_seen[6];
static bool subaru_avh_seq[6];
static bool subaru_avh_enabled;
static bool subaru_avh_done;
static unsigned int subaru_avh_count;
static uint32_t subaru_avh_start;
static uint32_t subaru_avh_sent;
static uint32_t subaru_avh_template_ts;
static uint32_t subaru_avh_stable_since;
static bool subaru_avh_stable;
static void subaru_avh_init(bool enabled) {
subaru_avh_enabled = enabled;
subaru_avh_done = false;
subaru_avh_count = 0U;
subaru_avh_start = microsecond_timer_get();
subaru_avh_sent = 0U;
subaru_avh_template_ts = 0U;
subaru_avh_stable_since = 0U;
subaru_avh_stable = false;
for (int i = 0; i < 6; i++) {
subaru_avh_seen[i] = false;
subaru_avh_seq[i] = false;
subaru_avh_ts[i] = 0U;
for (int j = 0; j < 8; j++) {
subaru_avh_data[i][j] = 0U;
}
}
}
static bool subaru_avh_ready(uint32_t now) {
bool ready = true;
for (int i = 0; i < 6; i++) {
ready &= subaru_avh_seen[i] && subaru_avh_seq[i] &&
(safety_get_ts_elapsed(now, subaru_avh_ts[i]) <= ((i == 0) ? 1500000U : 300000U));
}
const unsigned int rpm = ((unsigned int)subaru_avh_data[2][2] | ((unsigned int)subaru_avh_data[2][3] << 8U)) & 0x1FFFU;
ready &= (rpm >= 400U) && (subaru_avh_data[2][4] == 0U) && (subaru_avh_data[3][3] == 4U);
ready &= (subaru_avh_data[5][2] & 8U) != 0U;
ready &= !vehicle_moving && !controls_allowed;
return ready;
}
static void subaru_avh_rx(const CANPacket_t *msg) {
if (subaru_avh_enabled && !subaru_avh_done && (msg->bus == 1U)) {
const uint32_t now = microsecond_timer_get();
for (int i = 0; i < 6; i++) {
if (msg->addr == SUBARU_AVH_INPUTS[i]) {
if ((GET_LEN(msg) != 8U) || (subaru_get_checksum(msg) != subaru_compute_checksum(msg))) {
subaru_avh_done = true;
} else {
const uint8_t old_counter = subaru_avh_data[i][1] & 0xFU;
const uint8_t counter = msg->data[1] & 0xFU;
if (!subaru_avh_seen[i] || (counter != old_counter)) {
subaru_avh_seq[i] = subaru_avh_seen[i] && (counter == ((old_counter + 1U) & 0xFU));
subaru_avh_seen[i] = true;
subaru_avh_ts[i] = now;
for (int j = 0; j < 8; j++) {
subaru_avh_data[i][j] = msg->data[j];
}
}
if (((i == 0) && ((msg->data[2] & 3U) != 0U)) ||
((i == 1) && ((msg->data[5] & 0x20U) != 0U)) ||
((i == 2) && (msg->data[4] != 0U)) || ((i == 3) && (msg->data[3] != 4U)) ||
((i == 4) && (((GET_BYTES(msg, 1, 3) >> 4) & 0x1FFFU) != 0U ||
((GET_BYTES(msg, 3, 3) >> 1) & 0x1FFFU) != 0U ||
((GET_BYTES(msg, 4, 3) >> 6) & 0x1FFFU) != 0U ||
((GET_BYTES(msg, 6, 2) >> 3) & 0x1FFFU) != 0U))) {
subaru_avh_done = true;
}
}
}
}
if (controls_allowed || (safety_get_ts_elapsed(now, subaru_avh_start) > 30000000U)) {
subaru_avh_done = true;
}
if (!subaru_avh_ready(now)) {
subaru_avh_stable = false;
if (subaru_avh_count > 0U) {
subaru_avh_done = true;
}
} else if (!subaru_avh_stable) {
subaru_avh_stable = true;
subaru_avh_stable_since = now;
}
}
}
static bool subaru_avh_tx(const CANPacket_t *msg) {
const uint32_t now = microsecond_timer_get();
const uint32_t elapsed = safety_get_ts_elapsed(now, subaru_avh_start);
const bool second = subaru_avh_count == 1U;
bool allowed = subaru_avh_enabled && !subaru_avh_done && (subaru_avh_count < 2U) &&
(msg->bus == 1U) && (GET_LEN(msg) == 8U) && !safety_rx_checks_invalid &&
(elapsed >= 10000000U) && (elapsed <= 30000000U) && subaru_avh_ready(now) &&
subaru_avh_stable && (safety_get_ts_elapsed(now, subaru_avh_stable_since) >= 3000000U);
// Rejected generic RX frames may not reach our hook; invalidate their cached inputs too.
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
const RxCheck *check = &current_safety_config.rx_checks[i];
for (int j = 0; j < 6; j++) {
if (((unsigned int)check->msg[check->status.index].addr == SUBARU_AVH_INPUTS[j]) && (check->msg[check->status.index].bus == 1U)) {
allowed &= check->status.valid_checksum && (check->status.wrong_counters < MAX_WRONG_COUNTERS);
}
}
}
if (second) {
const uint32_t spacing = safety_get_ts_elapsed(now, subaru_avh_sent);
allowed &= (spacing >= 45000U) && (spacing <= 80000U) && (subaru_avh_ts[0] == subaru_avh_template_ts) &&
(safety_get_ts_elapsed(now, subaru_avh_ts[0]) <= 110000U);
} else {
allowed &= safety_get_ts_elapsed(now, subaru_avh_ts[0]) <= 30000U;
}
uint8_t sum = (uint8_t)(0xBBU + 6U);
for (int i = 1; i < 8; i++) {
uint8_t expected = subaru_avh_data[0][i];
if (i == 1) {
expected = (expected & 0xF0U) | ((expected + (second ? 2U : 1U)) & 0xFU);
} else if (i == 2) {
expected |= 2U;
} else {
// Preserve every unrelated payload bit.
}
allowed &= msg->data[i] == expected;
sum += expected;
}
allowed &= msg->data[0] == sum;
if (allowed) {
subaru_avh_count++;
subaru_avh_sent = now;
subaru_avh_template_ts = subaru_avh_ts[0];
subaru_avh_done = second;
}
return allowed;
}
+54 -2
View File
@@ -4,6 +4,10 @@
static bool tesla_longitudinal = false;
static bool tesla_coop_steering = false;
static bool tesla_aol_screen_button = false;
static bool tesla_screen_disengage_on_brake = false;
static bool tesla_touch_initialized = false;
static bool tesla_screen_cancel_pressed = false;
static bool tesla_stock_aeb = false;
#define TESLA_STEERING_DISENGAGE_TORQUE 500 // cNm
@@ -94,6 +98,27 @@ static bool tesla_get_quality_flag_valid(const CANPacket_t *msg) {
return valid;
}
static void tesla_rx_all_hook(const CANPacket_t *msg) {
if (tesla_aol_screen_button && (msg->bus == 1U) && (msg->addr == 0x229U) && (GET_LEN(msg) == 3)) {
const int stalk_status = (msg->data[1] >> 4) & 0x7U; // SCCM_rightStalkStatus
tesla_screen_cancel_pressed = (stalk_status == 1) || (stalk_status == 2); // UP_1, UP_2
if (tesla_screen_cancel_pressed) {
lkas_on = false;
}
}
// The add-on's UI message is asynchronous, not a required periodic safety input.
if (tesla_aol_screen_button && (msg->bus == 1U) && (msg->addr == 0x3DFU) && (GET_LEN(msg) == 8)) {
const bool screen_button = msg->data[3] == 3U; // UI_activeTouchPoints
if (tesla_touch_initialized && screen_button && !lkas_button_prev && !steering_disengage && !tesla_screen_cancel_pressed &&
!(tesla_screen_disengage_on_brake && brake_pressed)) {
lkas_on = !lkas_on;
}
// A touch held during startup is not an engagement request.
tesla_touch_initialized = true;
lkas_button_prev = screen_button;
}
}
static void tesla_rx_hook(const CANPacket_t *msg) {
if (msg->bus == 0U) {
@@ -112,6 +137,9 @@ static void tesla_rx_hook(const CANPacket_t *msg) {
steering_disengage = (hands_on_level >= 3) ||
(tesla_coop_steering && (SAFETY_ABS(torsion_bar_torque) > TESLA_STEERING_DISENGAGE_TORQUE)) ||
((eac_status == 0) && (eac_error_code == 9));
if (tesla_aol_screen_button && steering_disengage) {
lkas_on = false;
}
}
// Vehicle speed (DI_speed)
@@ -136,6 +164,9 @@ static void tesla_rx_hook(const CANPacket_t *msg) {
// Brake pressed
if (msg->addr == 0x39dU) {
brake_pressed = (msg->data[2] & 0x03U) == 2U;
if (tesla_screen_disengage_on_brake && brake_pressed) {
lkas_on = false;
}
}
// Cruise and Autopark/Summon state
@@ -164,9 +195,22 @@ static void tesla_rx_hook(const CANPacket_t *msg) {
(cruise_state == 7); // PRE_CANCEL
cruise_engaged = cruise_engaged && !tesla_autopark;
const bool cruise_engagement_started = cruise_engaged && !cruise_engaged_prev;
const bool cruise_cancelled = cruise_engaged_prev && !cruise_engaged && !brake_pressed;
pcm_cruise_check(cruise_engaged);
acc_main_on = ((cruise_state == 1) || cruise_engaged) && !tesla_autopark;
const bool acc_main_on_now = ((cruise_state == 1) || cruise_engaged) && !tesla_autopark;
if (tesla_aol_screen_button) {
if (cruise_engagement_started && !tesla_screen_cancel_pressed && !steering_disengage &&
!(tesla_screen_disengage_on_brake && brake_pressed)) {
lkas_on = true;
}
if (cruise_cancelled) {
lkas_on = false;
}
}
// Availability alone is not an engagement request on the screen-toggle path.
acc_main_on = !tesla_aol_screen_button && acc_main_on_now;
}
if (msg->addr == 0x155U) {
@@ -187,7 +231,8 @@ static void tesla_rx_hook(const CANPacket_t *msg) {
bool tesla_stock_lkas_now = steering_control_type == 2; // "LANE_KEEP_ASSIST"
// Only consider rising edges while controls are not allowed
if (tesla_stock_lkas_now && !tesla_stock_lkas_prev && !controls_allowed) {
if (tesla_stock_lkas_now && !tesla_stock_lkas_prev &&
!(controls_allowed || (tesla_aol_screen_button && aol_allowed))) {
tesla_stock_lkas = true;
}
if (!tesla_stock_lkas_now) {
@@ -337,6 +382,12 @@ static safety_config tesla_init(uint16_t param) {
};
SAFETY_UNUSED(param);
const uint16_t TESLA_FLAG_AOL_SCREEN_BUTTON = 512;
const uint16_t TESLA_FLAG_AOL_SCREEN_DISENGAGE_ON_BRAKE = 1024;
tesla_aol_screen_button = GET_FLAG(param, TESLA_FLAG_AOL_SCREEN_BUTTON);
tesla_screen_disengage_on_brake = tesla_aol_screen_button && GET_FLAG(param, TESLA_FLAG_AOL_SCREEN_DISENGAGE_ON_BRAKE);
tesla_touch_initialized = false;
tesla_screen_cancel_pressed = false;
#ifdef ALLOW_DEBUG
const uint16_t TESLA_FLAG_LONGITUDINAL_CONTROL = 1;
const uint16_t TESLA_FLAG_COOP_STEERING = 256;
@@ -377,6 +428,7 @@ static safety_config tesla_init(uint16_t param) {
const safety_hooks tesla_hooks = {
.init = tesla_init,
.rx_all = tesla_rx_all_hook,
.rx = tesla_rx_hook,
.tx = tesla_tx_hook,
.fwd = tesla_fwd_hook,
+16 -1
View File
@@ -404,7 +404,12 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
tx = false;
}
if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
// Camry Auto Hold replaces the camera AEB message only while stopped.
if ((msg->addr == 0x344U) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0)) {
if (vehicle_moving || gas_pressed || !acc_main_on) {
tx = false;
}
} else if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
tx = false;
}
}
@@ -571,11 +576,21 @@ static safety_config toyota_init(uint16_t param) {
return ret;
}
static bool toyota_fwd_hook(int bus_num, int addr) {
bool block_msg = false;
if (bus_num == 2) {
block_msg = (addr == 0x344) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0) &&
!vehicle_moving && !gas_pressed && acc_main_on;
}
return block_msg;
}
const safety_hooks toyota_hooks = {
.init = toyota_init,
.rx = toyota_rx_hook,
.rx_all = toyota_rx_all_hook,
.tx = toyota_tx_hook,
.fwd = toyota_fwd_hook,
.get_checksum = toyota_get_checksum,
.compute_checksum = toyota_compute_checksum,
.get_quality_flag_valid = toyota_get_quality_flag_valid,
+41 -3
View File
@@ -71,6 +71,27 @@ static uint16_t volvo_ecm_1_addr;
static uint16_t volvo_bus1_cruise_control_addr;
static bool volvo_c1;
#define VOLVO_STOCK_LCA5_FRAMES 4U
#define VOLVO_STOCK_LCA5_MAX_AGE_US 100000U
static uint8_t volvo_stock_lca5_data[VOLVO_STOCK_LCA5_FRAMES][8];
static uint32_t volvo_stock_lca5_ts[VOLVO_STOCK_LCA5_FRAMES];
static bool volvo_stock_lca5_valid[VOLVO_STOCK_LCA5_FRAMES];
static uint8_t volvo_stock_lca5_index;
static bool volvo_lca5_stock_relay(const CANPacket_t *msg) {
const uint32_t now = microsecond_timer_get();
bool matches_stock = false;
for (uint8_t i = 0U; i < VOLVO_STOCK_LCA5_FRAMES; i++) {
bool matches = volvo_stock_lca5_valid[i] &&
(safety_get_ts_elapsed(now, volvo_stock_lca5_ts[i]) <= VOLVO_STOCK_LCA5_MAX_AGE_US);
for (uint8_t byte = 0U; byte < 8U; byte++) {
matches &= msg->data[byte] == volvo_stock_lca5_data[i][byte];
}
matches_stock |= matches;
}
return matches_stock;
}
static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) {
return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]);
}
@@ -159,6 +180,15 @@ static void volvo_rx_hook(const CANPacket_t *msg) {
// Main bus (bus 0) messages
if (msg->bus == VOLVO_MAIN_BUS) {
if (msg->addr == VOLVO_LCA_5) {
for (uint8_t byte = 0U; byte < 8U; byte++) {
volvo_stock_lca5_data[volvo_stock_lca5_index][byte] = msg->data[byte];
}
volvo_stock_lca5_ts[volvo_stock_lca5_index] = microsecond_timer_get();
volvo_stock_lca5_valid[volvo_stock_lca5_index] = true;
volvo_stock_lca5_index = (volvo_stock_lca5_index + 1U) % VOLVO_STOCK_LCA5_FRAMES;
}
// Update brake pedal and cruise state from BCM2
if (msg->addr == VOLVO_LCA_2) {
// DBC: SG_ BRAKE_PEDAL_PRESSED_A : 47|1@0+ (-1,1) - inverted in DBC, so we invert raw bit
@@ -263,9 +293,13 @@ static bool volvo_tx_hook(const CANPacket_t *msg) {
// LCA frame also contains an angle-shaped field, but the imported controller
// deliberately leaves that field at the observed vehicle value.
if (msg->addr == VOLVO_LCA_5) {
const int desired_angle = volvo_lca_5_angle(msg);
tx &= SAFETY_ABS(desired_angle) <= VOLVO_MAX_ANGLE_CAN;
tx &= !steer_angle_cmd_checks(desired_angle, controls_allowed, VOLVO_ANGLE_STEERING_LIMITS);
if (!controls_allowed && volvo_lca5_stock_relay(msg)) {
desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -VOLVO_MAX_ANGLE_CAN, VOLVO_MAX_ANGLE_CAN);
} else {
const int desired_angle = volvo_lca_5_angle(msg);
tx &= SAFETY_ABS(desired_angle) <= VOLVO_MAX_ANGLE_CAN;
tx &= !steer_angle_cmd_checks(desired_angle, controls_allowed, VOLVO_ANGLE_STEERING_LIMITS);
}
}
// Keep the two torque-authority arms and the companion LCA angle bounded even
@@ -357,6 +391,10 @@ static bool volvo_tx_hook(const CANPacket_t *msg) {
static safety_config volvo_init(uint16_t param) {
bool spa = GET_FLAG(param, VOLVO_FLAG_SPA);
volvo_c1 = GET_FLAG(param, VOLVO_FLAG_C1);
volvo_stock_lca5_index = 0U;
for (uint8_t i = 0U; i < VOLVO_STOCK_LCA5_FRAMES; i++) {
volvo_stock_lca5_valid[i] = false;
}
if (volvo_c1) {
static const CanMsg VOLVO_C1_TX_MSGS[] = {
@@ -20,6 +20,7 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
init_segment(safety, msgs, safety_mode, param)
rx_tot, rx_invalid, tx_tot, tx_blocked, tx_controls, tx_controls_blocked = 0, 0, 0, 0, 0, 0
tx_lateral, tx_lateral_blocked = 0, 0
safety_tick_rx_invalid = False
blocked_addrs = Counter()
invalid_addrs = set()
@@ -38,14 +39,20 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
if msg.which() == 'sendcan':
for canmsg in msg.sendcan:
_msg = package_can_msg(canmsg)
# TX hooks can revoke permission on a violation. Count the permission
# before checking the message, including lateral-only AOL operation.
controls_allowed = safety.get_controls_allowed()
lateral_allowed = controls_allowed or safety.get_aol_allowed()
sent = safety.safety_tx_hook(_msg)
if not sent:
tx_blocked += 1
tx_controls_blocked += safety.get_controls_allowed()
tx_controls_blocked += controls_allowed
tx_lateral_blocked += lateral_allowed
blocked_addrs[canmsg.address] += 1
carlog.debug("blocked bus %d msg %d at %f" % (canmsg.src, canmsg.address, (msg.logMonoTime - start_t) / 1e9))
tx_controls += safety.get_controls_allowed()
tx_controls += controls_allowed
tx_lateral += lateral_allowed
tx_tot += 1
elif msg.which() == 'can':
# ignore msgs we sent
@@ -68,9 +75,11 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
print("total msgs with controls allowed:", tx_controls)
print("blocked msgs:", tx_blocked)
print("blocked with controls allowed:", tx_controls_blocked)
print("total msgs with lateral allowed:", tx_lateral)
print("blocked with lateral allowed:", tx_lateral_blocked)
print("blocked addrs:", blocked_addrs)
return tx_controls_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
return tx_lateral_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
if __name__ == "__main__":
@@ -0,0 +1,63 @@
import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
@pytest.fixture
def replay_module(monkeypatch):
# Load the sibling source explicitly, without native safety libraries or a
# host-runtime snapshot. These tests isolate replay accounting, not CAN rules.
monkeypatch.setitem(sys.modules, "opendbc.car.carlog", SimpleNamespace(carlog=Mock()))
monkeypatch.setitem(sys.modules, "opendbc.safety.tests.libsafety", SimpleNamespace(libsafety_py=SimpleNamespace()))
monkeypatch.setitem(sys.modules, "opendbc.safety.tests.safety_replay.helpers",
SimpleNamespace(package_can_msg=lambda msg: msg, init_segment=Mock()))
spec = importlib.util.spec_from_file_location("replay_drive_accounting", Path(__file__).with_name("replay_drive.py"))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.tqdm = lambda msgs: msgs
return module
@pytest.mark.parametrize("controls,aol,accepted,post_controls,post_aol", [
(False, True, False, False, True), # AOL-only denial must fail replay.
(False, True, False, False, False), # A TX hook can revoke AOL permission.
(True, False, False, False, False), # A TX hook can revoke controls permission.
(False, False, False, False, False), # Expected inactive blocks remain allowed.
(False, False, False, True, True), # Post-hook permission must not misclassify a block.
(False, True, True, False, True),
(True, False, True, True, False),
(True, True, True, True, True), # Count overlapping permissions only once.
])
def test_tx_authorization_accounted_before_hook(replay_module, capsys, controls, aol, accepted, post_controls, post_aol):
state = SimpleNamespace(controls=controls, aol=aol)
def tx_hook(msg):
state.controls = post_controls
state.aol = post_aol
return accepted
safety = Mock()
safety.set_safety_hooks.return_value = 0
safety.get_controls_allowed.side_effect = lambda: state.controls
safety.get_aol_allowed.side_effect = lambda: state.aol
safety.safety_tx_hook.side_effect = tx_hook
replay_module.libsafety_py.libsafety = safety
packet = SimpleNamespace(address=0x488, src=0, dat=b"\x00" * 4)
msg = SimpleNamespace(logMonoTime=0, sendcan=[packet], which=lambda: "sendcan")
result = replay_module.replay_drive([msg], 10, 0, 0)
lateral_allowed = controls or aol
assert result == (accepted or not lateral_allowed)
safety.safety_tx_hook.assert_called_once_with(packet)
output = capsys.readouterr().out
assert "total openpilot msgs: 1\n" in output
assert f"total msgs with controls allowed: {int(controls)}\n" in output
assert f"blocked msgs: {int(not accepted)}\n" in output
assert f"blocked with controls allowed: {int(controls and not accepted)}\n" in output
assert f"total msgs with lateral allowed: {int(lateral_allowed)}\n" in output
assert f"blocked with lateral allowed: {int(lateral_allowed and not accepted)}\n" in output
@@ -467,6 +467,92 @@ class TestFordCANFDStockSafety(TestFordSafetyBase):
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
self.safety.init_tests()
class TestFordMachEExtendedCurvatureSafety(TestFordCANFDStockSafety):
def setUp(self):
self.packer = CANPackerSafety("ford_lincoln_base_pt")
self.safety = libsafety_py.libsafety
self.safety.set_safety_hooks(CarParams.SafetyModel.ford,
FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_CURVATURE)
self.safety.init_tests()
def test_mach_e_extended_curvature_error(self):
self.safety.set_controls_allowed(True)
self._reset_curvature_measurement(0.0, 12.0)
self.assertTrue(self._tx(self._extended_lka_msg()))
for curvature, allowed in ((0.0058, True), (0.0062, False), (-0.0058, True), (-0.0062, False)):
self._set_prev_desired_angle(curvature)
self.assertEqual(allowed, self._tx(self._lat_ctl_msg(True, 0.0, 0.0, curvature, 0.0)))
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.02, 0.005, 0.0)))
def test_mach_e_bounded_path_angle_assist(self):
self.safety.set_controls_allowed(True)
self._reset_curvature_measurement(0.02, 7.5)
self._set_prev_desired_angle(0.02)
self.assertTrue(self._tx(self._extended_lka_msg()))
for path_angle in (0.055, 0.11, 0.15):
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, path_angle, 0.02, 0.0)))
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.161, 0.02, 0.0)))
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, 0.02, 0.0)))
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, -0.055, 0.02, 0.0)))
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.018, 0.0)))
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.12, 0.02, 0.0)))
self._reset_curvature_measurement(0.02, 9.0)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.02, 0.0)))
def test_mach_e_aol_keeps_curvature_without_path_angle_assist(self):
self.safety.set_alternative_experience(32)
self._rx(self._toggle_aol(True))
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self._tx(self._extended_lka_msg()))
for sign in (-1, 1):
self._reset_curvature_measurement(sign * 0.02, 7.5)
self._set_prev_desired_angle(sign * 0.02)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, sign * 0.055, sign * 0.02, 0.0)))
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, sign * 0.02, 0.0)))
def test_mach_e_accelerator_assist_requires_retained_engagement(self):
for sign in (-1, 1):
self.setUp()
self.safety.set_alternative_experience(32)
self._rx(self._toggle_aol(True))
self._reset_curvature_measurement(sign * 0.02, 7.5)
self._set_prev_desired_angle(sign * 0.02)
self._rx(self._pcm_status_msg(False))
self._rx(self._pcm_status_msg(True))
self.assertTrue(self._tx(self._extended_lka_msg()))
for gas in (0.0, 25.0, 0.0):
self._rx(self._user_gas_msg(gas))
self.assertTrue(self.safety.get_controls_allowed())
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, sign * 0.055, sign * 0.02, 0.0)))
self._rx(self._toggle_aol(True))
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, sign * 0.055, sign * 0.02, 0.0)))
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, sign * 0.02, 0.0)))
self._rx(self._pcm_status_msg(True))
self._rx(self._user_brake_msg(True))
self.assertFalse(self.safety.get_controls_allowed())
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, sign * 0.055, sign * 0.02, 0.0)))
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, sign * 0.02, 0.0)))
def test_other_canfd_fords_keep_original_error(self):
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
self.safety.init_tests()
self.safety.set_controls_allowed(True)
self._reset_curvature_measurement(0.0, 12.0)
self.assertTrue(self._tx(self._extended_lka_msg()))
self._set_prev_desired_angle(0.0058)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, 0.0058, 0.0)))
self._reset_curvature_measurement(0.02, 7.5)
self._set_prev_desired_angle(0.02)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.02, 0.0)))
class TestFordStockSafety(TestFordSafetyBase):
STEER_MESSAGE = MSG_LateralMotionControl
STOCK_LONGITUDINAL = True
@@ -635,6 +635,23 @@ class TestHyundaiLongitudinalAolLkasOnEngageSafety(HyundaiAolLkasOnEngageBase, T
HyundaiSafetyFlags.LONG | HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE)
self.safety.init_tests()
def test_main_off_after_brake_keeps_lateral_permission(self):
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
self._rx(self._button_msg(Buttons.NONE, main_button=1))
self._rx(self._button_msg(Buttons.NONE, main_button=0))
self._rx(self._button_msg(Buttons.SET))
self._rx(self._button_msg(Buttons.NONE))
self._rx(self._user_brake_msg(True))
self._rx(self._button_msg(Buttons.NONE, main_button=1))
self._rx(self._button_msg(Buttons.NONE, main_button=0))
self.assertFalse(self.safety.get_controls_allowed())
self.assertFalse(self.safety.get_acc_main_on())
self.assertTrue(self.safety.get_lkas_on())
self._set_prev_torque(0)
self.assertTrue(self._tx(self._torque_cmd_msg(self.MAX_RATE_UP)))
class TestHyundaiLongitudinalAolMainLkasOnEngageSafety(TestHyundaiLongitudinalSafety):
def setUp(self):
@@ -959,5 +959,85 @@ class TestHyundaiCanfdLKASteeringAolLkasOnEngageEV(HyundaiAolLkasOnEngageStockBa
self.safety.init_tests()
class TestSportageNoStockLka(unittest.TestCase):
TX_MSGS = None # Supplemental transition tests, not a separate safety mode.
PARAM = (HyundaiSafetyFlags.CANFD_LKA_STEERING | HyundaiSafetyFlags.CANFD_LKA_STEERING_ALT |
HyundaiSafetyFlags.CANFD_ANGLE_STEERING | HyundaiSafetyFlags.HYBRID_GAS)
def setUp(self):
self.safety = libsafety_py.libsafety
self.packer = CANPackerSafety("hyundai_canfd_generated")
self._init(True)
def _init(self, suppress):
param = self.PARAM | (HyundaiStarPilotSafetyFlags.CANFD_NO_STOCK_LKA if suppress else 0)
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, param)
self.safety.init_tests()
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
def _speed(self, speed):
for _ in range(common.MAX_SAMPLE_VALS):
self.safety.safety_rx_hook(self.packer.make_can_msg_safety(
"WHEEL_SPEEDS", 1, {f"WHL_Spd{pos}Val": speed for pos in ("FL", "FR", "RL", "RR")}))
def _toggle(self):
for pressed in (1, 0):
self.safety.safety_rx_hook(self.packer.make_can_msg_safety("CRUISE_BUTTONS", 1, {"LDA_BTN": pressed}))
def _steer(self, active, gain=None):
return self.packer.make_can_msg_safety("LKAS_ALT", 0, {
"LKAS_ANGLE_ACTIVE": 2 if active else 1,
"ADAS_StrAnglReqVal": 0,
"ADAS_ACIAnglTqRedcGainVal": (0.4 if active else 0.0) if gain is None else gain,
"Damping_Gain": 100,
})
def test_stock_scc_buttons_require_engagement(self):
resume = self.packer.make_can_msg_safety("CRUISE_BUTTONS", 1, {"CRUISE_BUTTONS": Buttons.RESUME})
set_button = self.packer.make_can_msg_safety("CRUISE_BUTTONS", 1, {"CRUISE_BUTTONS": Buttons.SET})
self.assertFalse(self.safety.safety_tx_hook(resume))
self.assertFalse(self.safety.safety_tx_hook(set_button))
self.safety.safety_rx_hook(set_button)
self.safety.safety_rx_hook(self.packer.make_can_msg_safety("SCC_CONTROL", 1, {"ACCMode": 1}))
self.assertTrue(self.safety.get_controls_allowed())
self.assertTrue(self.safety.safety_tx_hook(resume))
self.assertTrue(self.safety.safety_tx_hook(set_button))
self.safety.safety_rx_hook(self.packer.make_can_msg_safety("SCC_CONTROL", 1, {"ACCMode": 0}))
self.assertFalse(self.safety.safety_tx_hook(resume))
self.assertFalse(self.safety.safety_tx_hook(set_button))
def test_aol_toggle_keeps_stock_blocked_and_inactive_status_allowed(self):
self._speed(30)
for expected_aol in (False, True, False, True, False):
if expected_aol != self.safety.get_aol_allowed():
self._toggle()
self.assertEqual(expected_aol, self.safety.get_aol_allowed())
for addr in (0x110, 0x362):
self.assertEqual(-1, self.safety.safety_fwd_hook(2, addr))
self.assertTrue(self.safety.safety_tx_hook(self._steer(False)))
self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x362, 32)))
self.assertEqual(expected_aol, self.safety.safety_tx_hook(self._steer(True)))
self.assertFalse(self.safety.safety_tx_hook(self._steer(False, gain=0.4)))
def test_standstill_does_not_allow_active_steering(self):
self._speed(0)
self._toggle()
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self.safety.safety_tx_hook(self._steer(True)))
self.assertTrue(self.safety.safety_tx_hook(self._steer(False)))
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x110))
def test_unflagged_handoff_and_reinitialization_unchanged(self):
self._init(False)
self._speed(30)
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x110))
self.assertFalse(self.safety.safety_tx_hook(self._steer(False)))
self._toggle()
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x110))
self.assertTrue(self.safety.safety_tx_hook(self._steer(True)))
if __name__ == "__main__":
unittest.main()
@@ -4,6 +4,7 @@ from opendbc.can import CANPacker
from opendbc.car import create_gas_interceptor_command
from opendbc.car.structs import CarParams
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_hyundai import checksum
@pytest.mark.parametrize("param", [0x9405, 0x9C05, 0x9401, 0x1005, 0])
@@ -20,8 +21,9 @@ def test_ray_pedal_tx_isolation_and_limits(param):
has_ray_signature = param in (0x9405, 0x9C05)
assert tx(0) is has_ray_signature
assert tx(0.35) is has_ray_signature
assert not tx(0.36) # above the Ray-only initial command cap
assert tx(0.55) is has_ray_signature
assert not tx(0.56)
assert not tx(0.70)
assert not tx(1.0)
if has_ray_signature:
@@ -82,3 +84,49 @@ def test_non_ray_hyundai_ev_keeps_native_driver_gas_detection():
native_gas = bytes.fromhex("004e008000ae0700")
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x371, 0, native_gas))
assert safety.get_gas_pressed_prev()
@pytest.mark.parametrize("controls_allowed", [False, True])
def test_ray_native_cruise_cancel_allowed_during_pedal_override(controls_allowed):
safety = libsafety_py.libsafety
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
safety.init_tests()
safety.set_controls_allowed(controls_allowed)
safety.set_gas_pressed_prev(True)
packer = CANPacker("hyundai_can_refresh_generated")
addr, dat, bus = packer.make_can_msg("CLU11", 0, {"CF_Clu_CruiseSwState": 4})
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
pedal_packer = CANPacker("hyundai_kia_ray_pedal")
addr, dat, bus = create_gas_interceptor_command(pedal_packer, 0.1, 3)
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
def test_ray_standstill_launch_obeys_hardware_brake_override():
safety = libsafety_py.libsafety
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
safety.init_tests()
packer = CANPacker("hyundai_can_refresh_generated")
pedal_packer = CANPacker("hyundai_kia_ray_pedal")
def rx(name, values):
addr, dat, bus = checksum(packer.make_can_msg(name, 0, values))
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
def tx(gas):
addr, dat, bus = create_gas_interceptor_command(pedal_packer, gas, 0)
return safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
rx("WHL_SPD11", {"WHL_SPD_FL": 0, "WHL_SPD_RR": 0})
assert not safety.get_vehicle_moving()
rx("TCS13", {"DriverOverride": 2})
safety.set_controls_allowed(True)
assert safety.get_brake_pressed_prev()
assert tx(0)
assert not tx(0.012)
rx("TCS13", {"DriverOverride": 0})
assert not safety.get_brake_pressed_prev()
assert tx(0.012)
rx("TCS13", {"DriverOverride": 2})
assert not tx(0.012)
assert tx(0)
@@ -0,0 +1,107 @@
import pytest
from opendbc.car.structs import CarParams
from opendbc.car.subaru.avh import AVH_REQUEST, AVH_STATUS, INPUTS, avh_request, checksum
from opendbc.car.subaru.tests.test_avh import sample
from opendbc.car.subaru.values import SubaruSafetyFlags
from opendbc.safety.tests.libsafety import libsafety_py
FLAGS = int(SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.FIXED_ANGLE_LIMITS |
SubaruSafetyFlags.STOP_START_BUTTON | SubaruSafetyFlags.AVH_STARTUP)
def packet(address, data, bus=1):
return libsafety_py.make_CANPacket(address, bus, data)
@pytest.fixture
def safety():
s = libsafety_py.libsafety
s.set_timer(0)
assert s.set_safety_hooks(CarParams.SafetyModel.subaru, FLAGS) == 0
s.set_controls_allowed(False)
for tick in range(101):
s.set_timer(tick * 100_000)
for address in INPUTS:
if address != AVH_REQUEST or tick % 10 == 0:
assert s.safety_rx_hook(packet(address, sample(address, tick // 10 if address == AVH_REQUEST else tick)))
return s
def request(step=1):
return avh_request(sample(AVH_REQUEST, 10), step)[1]
def test_pair_and_third_frame_blocked(safety):
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
safety.set_timer(10_050_000)
assert safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
safety.set_timer(10_100_000)
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
@pytest.mark.parametrize('byte', range(8))
def test_payload_mutation_blocked(safety, byte):
data = bytearray(request())
data[byte] ^= 4
if byte:
data[0] = checksum(AVH_REQUEST, data)
assert not safety.safety_tx_hook(packet(AVH_REQUEST, data))
@pytest.mark.parametrize('bus', [0, 2])
def test_wrong_bus_blocked(safety, bus):
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(), bus))
@pytest.mark.parametrize('delay', [44_999, 80_001])
def test_followup_timing(safety, delay):
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
safety.set_timer(10_000_000 + delay)
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
@pytest.mark.parametrize('address,offset,value', [(AVH_REQUEST, 2, 1), (AVH_STATUS, 5, 32),
(0x40, 4, 1), (0x48, 3, 3), (0x13A, 2, 1)])
def test_abort_on_manual_ack_or_movement(safety, address, offset, value):
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
safety.set_timer(10_050_000)
data = bytearray(sample(address, 11 if address == AVH_REQUEST else 101))
data[offset] = value
data[0] = checksum(address, data)
assert safety.safety_rx_hook(packet(address, data))
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
def test_stale_template_and_status_tx_blocked(safety):
assert not safety.safety_tx_hook(packet(AVH_STATUS, sample(AVH_STATUS, 1)))
safety.set_timer(10_030_001)
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
@pytest.mark.parametrize('flags', [FLAGS & ~1024, FLAGS | 32, FLAGS | 2, FLAGS & ~16, FLAGS | 512])
def test_permission_gates(safety, flags):
assert safety.set_safety_hooks(CarParams.SafetyModel.subaru, flags) == 0
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
@pytest.mark.parametrize('reason', ['new_template', 'corrupt', 'engaged', 'expired', 'duplicate'])
def test_extra_failure_gates(safety, reason):
if reason == 'engaged':
safety.set_controls_allowed(True)
elif reason == 'expired':
safety.set_timer(30_000_001)
elif reason == 'duplicate':
safety.set_timer(10_040_000)
assert safety.safety_rx_hook(packet(AVH_REQUEST, sample(AVH_REQUEST, 10)))
elif reason == 'corrupt':
data = bytearray(sample(AVH_REQUEST, 11))
data[0] ^= 1
safety.safety_rx_hook(packet(AVH_REQUEST, data))
else:
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
safety.set_timer(10_050_000)
assert safety.safety_rx_hook(packet(AVH_REQUEST, sample(AVH_REQUEST, 11)))
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
return
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
@@ -0,0 +1,257 @@
import unittest
from opendbc.car.structs import CarParams
from opendbc.car.tesla.values import TeslaSafetyFlags
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from opendbc.safety.tests.common import CANPackerSafety, make_msg
from opendbc.safety.tests import test_tesla
class TestTeslaScreenButton(unittest.TestCase):
TX_MSGS = None
def setUp(self):
self.helper = test_tesla.TestTeslaStockSafety()
self.helper.setUp()
self.safety = self.helper.safety
self.vehicle_packer = CANPackerSafety("tesla_model3_vehicle")
self.init_safety()
def init_safety(self, flag=True, aol=True, long_control=False, disengage_on_brake=False, cooperative=False):
param = (TeslaSafetyFlags.AOL_SCREEN_BUTTON if flag else 0) | (TeslaSafetyFlags.LONG_CONTROL if long_control else 0)
if cooperative:
param |= TeslaSafetyFlags.COOP_STEERING
if disengage_on_brake:
param |= TeslaSafetyFlags.AOL_SCREEN_DISENGAGE_ON_BRAKE
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, param), 0)
self.safety.init_tests()
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL if aol else 0)
self.helper._rx(self.helper._pcm_status_msg(False))
self.safety.set_angle_meas(0, 0)
self.safety.set_desired_angle_last(0)
def touch(self, fingers, bus=1):
self.helper._rx(self.vehicle_packer.make_can_msg_safety("UI_status2", bus, {"UI_activeTouchPoints": fingers}))
def gesture(self):
self.touch(0)
self.touch(3)
def stalk(self, status, bus=1):
self.helper._rx(self.vehicle_packer.make_can_msg_safety("SCCM_rightStalk", bus, {"SCCM_rightStalkStatus": status}))
def test_low_speed_acc_availability_does_not_clear_gesture(self):
for cooperative in (False, True):
for initial_available in (False, True):
with self.subTest(cooperative=cooperative, initial_available=initial_available):
self.init_safety(cooperative=cooperative)
msg = self.helper.packer.make_can_msg_safety("DI_state", 0, {"DI_cruiseState": 1 if initial_available else 0})
self.helper._rx(msg)
self.assertFalse(self.safety.get_aol_allowed())
self.gesture()
for available in (True, False) * 20:
self.helper._rx(self.helper._speed_msg(3.0))
self.helper._rx(self.helper._speed_msg_2(3.0))
msg = self.helper.packer.make_can_msg_safety("DI_state", 0, {"DI_cruiseState": 1 if available else 0})
self.helper._rx(msg)
self.assertTrue(self.safety.get_lkas_on())
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self.helper._tx(self.helper._angle_cmd_msg(0)))
self.gesture()
for available in (True, False) * 20:
msg = self.helper.packer.make_can_msg_safety("DI_state", 0, {"DI_cruiseState": 1 if available else 0})
self.helper._rx(msg)
self.assertFalse(self.safety.get_lkas_on())
self.assertFalse(self.safety.get_aol_allowed())
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_cruise_engagement_arms_lateral_and_brake_respects_option(self):
for disengage_on_brake in (False, True):
with self.subTest(disengage_on_brake=disengage_on_brake):
self.init_safety(disengage_on_brake=disengage_on_brake)
self.helper._rx(self.helper._pcm_status_msg(True))
self.assertTrue(self.safety.get_lkas_on())
self.assertTrue(self.safety.get_aol_allowed())
self.helper._rx(self.helper._user_brake_msg(True))
self.helper._rx(self.helper._pcm_status_msg(False))
self.helper._rx(self.helper._user_brake_msg(False))
self.assertEqual(self.safety.get_lkas_on(), not disengage_on_brake)
self.assertEqual(self.safety.get_aol_allowed(), not disengage_on_brake)
self.assertFalse(self.safety.get_controls_allowed())
def test_cruise_engagement_does_not_override_screen_disengagement_inputs(self):
for blocked_by in ("brake", "steering", "cancel"):
with self.subTest(blocked_by=blocked_by):
self.init_safety(disengage_on_brake=True)
if blocked_by == "brake":
self.helper._rx(self.helper._user_brake_msg(True))
elif blocked_by == "steering":
self.helper._rx(self.helper._angle_meas_msg(0, hands_on_level=3))
else:
self.stalk(1)
self.helper._rx(self.helper._pcm_status_msg(True))
self.assertFalse(self.safety.get_lkas_on())
self.assertFalse(self.safety.get_aol_allowed())
def test_stalk_cancel_clears_gesture_and_blocks_tap_while_held(self):
for status in (1, 2):
with self.subTest(status=status):
self.init_safety()
self.gesture()
self.stalk(status)
self.assertFalse(self.safety.get_lkas_on())
self.assertFalse(self.safety.get_aol_allowed())
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
self.gesture()
self.assertFalse(self.safety.get_lkas_on())
self.stalk(0)
self.assertFalse(self.safety.get_lkas_on())
self.gesture()
self.assertTrue(self.safety.get_lkas_on())
def test_non_cancel_stalk_positions_do_not_clear_gesture(self):
self.gesture()
for status in (0, 3, 4, 5, 6, 7):
self.stalk(status)
self.assertTrue(self.safety.get_lkas_on())
def test_stalk_cancel_is_addon_only(self):
self.gesture()
for bus in (0, 2, 3):
self.stalk(1, bus)
self.assertTrue(self.safety.get_lkas_on())
for length in (0, 1, 2, 4, 5, 8):
msg = make_msg(1, 0x229, length)
if length > 1:
msg[0].data[1] = 0x10
self.helper._rx(msg)
self.assertTrue(self.safety.get_lkas_on())
self.init_safety(flag=False)
self.helper._rx(self.helper._pcm_status_msg(True))
self.stalk(1)
self.assertTrue(self.safety.get_controls_allowed())
def test_gesture_enables_only_lateral(self):
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
self.gesture()
self.assertTrue(self.safety.get_aol_allowed())
self.assertTrue(self.safety.get_lkas_on())
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self.helper._tx(self.helper._angle_cmd_msg(0)))
self.assertFalse(self.helper._tx(self.helper._long_control_msg(10, acc_state=2, accel_limits=(0, 1))))
self.gesture()
self.assertFalse(self.safety.get_aol_allowed())
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_does_not_enable_alpha_long_actuation(self):
self.init_safety(long_control=True)
self.gesture()
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self.safety.get_controls_allowed())
self.assertFalse(self.helper._tx(self.helper._long_control_msg(10, acc_state=2, accel_limits=(0, 1))))
def test_flag_and_aol_are_required(self):
for flag, aol in ((False, False), (False, True), (True, False)):
with self.subTest(flag=flag, aol=aol):
self.init_safety(flag=flag, aol=aol)
self.gesture()
self.assertFalse(self.safety.get_aol_allowed())
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_wrong_bus_and_length_do_not_toggle(self):
for bus in (0, 2, 3):
self.gesture_on_wrong_bus(bus)
self.assertFalse(self.safety.get_lkas_on())
self.touch(0)
for length in (0, 3, 4, 5, 6, 7):
msg = make_msg(1, 0x3DF, length)
if length > 3:
msg[0].data[3] = 3
self.helper._rx(msg)
self.assertFalse(self.safety.get_lkas_on())
def gesture_on_wrong_bus(self, bus):
self.touch(0, bus)
self.touch(3, bus)
def test_held_touch_and_other_finger_counts(self):
for fingers in (3, 3, 0, 1, 2, 4, 5, 255, 0):
self.touch(fingers)
self.assertFalse(self.safety.get_lkas_on())
self.touch(3)
for _ in range(20):
self.touch(3)
self.assertTrue(self.safety.get_lkas_on())
self.touch(0)
self.touch(3)
self.assertFalse(self.safety.get_lkas_on())
def test_brake_disables_long_but_not_gesture_lateral(self):
self.gesture()
self.helper._rx(self.helper._user_brake_msg(True))
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_opt_in_brake_disengage_needs_brake_release_and_new_gesture(self):
self.init_safety(disengage_on_brake=True)
self.gesture()
self.helper._rx(self.helper._user_brake_msg(True))
self.assertFalse(self.safety.get_aol_allowed())
self.gesture()
self.assertFalse(self.safety.get_aol_allowed())
self.helper._rx(self.helper._user_brake_msg(False))
self.assertFalse(self.safety.get_aol_allowed())
self.gesture()
self.assertTrue(self.safety.get_aol_allowed())
def test_steering_override_requires_a_new_gesture(self):
self.gesture()
self.helper._rx(self.helper._angle_meas_msg(0, hands_on_level=3))
self.assertFalse(self.safety.get_aol_allowed())
self.gesture()
self.assertFalse(self.safety.get_aol_allowed())
self.helper._rx(self.helper._angle_meas_msg(0))
self.assertFalse(self.safety.get_aol_allowed())
self.gesture()
self.assertTrue(self.safety.get_aol_allowed())
def test_cancelled_cruise_clears_gesture_latch(self):
self.gesture()
self.helper._rx(self.helper._pcm_status_msg(True))
self.helper._rx(self.helper._pcm_status_msg(False))
self.assertFalse(self.safety.get_aol_allowed())
self.assertFalse(self.safety.get_lkas_on())
def test_safety_reinitialization_clears_touch_latch(self):
self.gesture()
self.init_safety()
self.touch(3)
self.assertFalse(self.safety.get_aol_allowed())
def test_stock_lkas_does_not_block_authorized_gesture_steering(self):
self.gesture()
self.helper._rx(self.helper._angle_cmd_msg(0, state=2, bus=2))
self.assertTrue(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_stock_lkas_still_blocks_when_not_authorized(self):
self.helper._rx(self.helper._angle_cmd_msg(0, state=2, bus=2))
self.gesture()
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_autopark_still_blocks_steering(self):
self.helper._rx(self.helper._pcm_status_msg(False, autopark_state=3))
self.gesture()
self.assertFalse(self.helper._tx(self.helper._angle_cmd_msg(0)))
def test_aeb_still_blocks_longitudinal_and_is_forwarded(self):
self.init_safety(long_control=True)
self.gesture()
self.helper._rx(self.helper._long_control_msg(0, aeb_event=1, bus=2))
self.assertFalse(self.helper._tx(self.helper._long_control_msg(0, acc_state=13)))
self.assertEqual(self.safety.safety_fwd_hook(2, 0x2B9), 0)
if __name__ == "__main__":
unittest.main()
@@ -147,6 +147,34 @@ class TestToyotaSafetyBase(common.CarSafetyTest, common.LongitudinalAccelSafetyT
self.safety.set_alternative_experience(0)
self.assertFalse(self._tx(hold_msg))
def test_auto_brake_hold_aeb_replacement_only_at_standstill(self):
if (not self.LONGITUDINAL or
self.safety.get_current_safety_param() & (ToyotaSafetyFlags.STOCK_LONGITUDINAL.value | ToyotaSafetyFlags.SECOC.value)):
raise unittest.SkipTest("Toyota AEB Auto Hold requires non-SecOC openpilot longitudinal control")
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALLOW_AEB)
hold_msg = libsafety_py.make_CANPacket(0x344, 0, b"\xfd\x80\x00\x00\x00\x00\x00\xcc")
self._rx(self._speed_msg(0))
self._rx(self._toggle_aol(True))
self._rx(self._user_gas_msg(False))
self.assertTrue(self._tx(hold_msg))
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x344))
self._rx(self._speed_msg(1.0))
self.assertFalse(self._tx(hold_msg))
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
self._rx(self._speed_msg(0))
self._rx(self._user_gas_msg(True))
self.assertFalse(self._tx(hold_msg))
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
self._rx(self._user_gas_msg(False))
self._rx(self._toggle_aol(False))
self.assertFalse(self._tx(hold_msg))
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
# Only allow LTA msgs with no actuation
def test_lta_steer_cmd(self):
for engaged, req, req2, torque_wind_down, angle in itertools.product([True, False],
@@ -177,6 +177,68 @@ class TestVolvoSafetyBase(common.CarSafetyTest):
self.assertTrue(self._tx(self._angle_cmd_msg(10)))
self.assertFalse(self._tx(self._angle_cmd_msg(20)))
STOCK_LCA5 = bytes.fromhex("88c04cef1190ba00")
def _stock_lca5(self, data=None, bus=VOLVO_PARTY_BUS):
return libsafety_py.make_CANPacket(VOLVO_LCA_5, bus, self.STOCK_LCA5 if data is None else data)
def test_inactive_lca5_stock_relay_requires_unchanged_received_frame(self):
self._reset_angle_measurement(10)
self.safety.set_timer(100000)
self.assertFalse(self._tx(self._stock_lca5()))
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.assertTrue(self._tx(self._stock_lca5()))
self.assertEqual(self.safety.get_desired_angle_last(), round(10 / 0.05596))
for byte in range(8):
altered = bytearray(self.STOCK_LCA5)
altered[byte] ^= 1
self.assertFalse(self._tx(self._stock_lca5(bytes(altered))), f"altered {byte=}")
self.assertFalse(self._tx(self._stock_lca5(bus=VOLVO_MAIN_BUS)))
self.assertFalse(self._tx(self._stock_lca5(bus=VOLVO_PT_BUS)))
def test_inactive_lca5_stock_relay_wrong_rx_bus_and_length(self):
self._rx(self._stock_lca5(bus=VOLVO_PT_BUS))
self._rx(self._stock_lca5(self.STOCK_LCA5[:7], bus=VOLVO_MAIN_BUS))
self.assertFalse(self._tx(self._stock_lca5()))
def test_inactive_lca5_stock_relay_expires_across_timer_wrap(self):
start = 0xFFFF0000
self.safety.set_timer(start)
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.safety.set_timer((start + 100000) & 0xFFFFFFFF)
self.assertTrue(self._tx(self._stock_lca5()))
self.safety.set_timer((start + 100001) & 0xFFFFFFFF)
self.assertFalse(self._tx(self._stock_lca5()))
def test_inactive_lca5_stock_relay_history_is_bounded(self):
frames = []
for i in range(5):
data = bytearray(self.STOCK_LCA5)
data[3] = (data[3] + i) & 0xFF
frames.append(bytes(data))
self.safety.set_timer(i * 20000)
self._rx(self._stock_lca5(frames[-1], bus=VOLVO_MAIN_BUS))
self.assertFalse(self._tx(self._stock_lca5(frames[0])))
for data in frames[1:]:
self.assertTrue(self._tx(self._stock_lca5(data)))
def test_inactive_lca5_stock_relay_cleared_on_safety_init(self):
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.assertTrue(self._tx(self._stock_lca5()))
self.safety.set_safety_hooks(SAFETY_VOLVO, self.SAFETY_PARAM)
self.assertFalse(self._tx(self._stock_lca5()))
def test_stock_lca5_placeholder_does_not_bypass_active_angle_limits(self):
self._reset_angle_measurement(10)
self._reset_speed_measurement(50)
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.assertTrue(self._tx(self._stock_lca5()))
self.safety.set_controls_allowed(True)
self.assertFalse(self._tx(self._stock_lca5()))
self.assertTrue(self._tx(self._angle_cmd_msg(10.4)))
self.assertFalse(self._tx(self._angle_cmd_msg(11)))
def test_angle_tx_rate_matches_controller_cadence(self):
"""LCA_5 is 50 Hz, so each frame may contain two 100 Hz controller steps."""
self._reset_speed_measurement(50)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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