mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-01 03:24:05 +08:00
Compare commits
268 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0defb30c6a | |||
| bcafa532a2 | |||
| 667fb666b9 | |||
| 32f49f8527 | |||
| 8e521e6992 | |||
| f8214fe6c6 | |||
| 258cbb76ec | |||
| c7c07799d3 | |||
| 7f7dbc6a65 | |||
| 2e68e2d926 | |||
| 0623c1a0f2 | |||
| 69999fa6cc | |||
| 1f60231a84 | |||
| a651454e7d | |||
| 8928743749 | |||
| 148cba7260 | |||
| d9127cf936 | |||
| 6d51c06692 | |||
| 3a5d376b54 | |||
| c7fb5d545d | |||
| 9d36f4cd0d | |||
| 8693affef0 | |||
| df68b82a5e | |||
| 3980401632 | |||
| 7f48d7c53b | |||
| 6e83349fe2 | |||
| 89c189e528 | |||
| 698496dd60 | |||
| 4d4ed2b3da | |||
| a49d8772f3 | |||
| 639450281e | |||
| dc8d448b35 | |||
| 61fd19556f | |||
| 956aceaf0a | |||
| 51d09a6274 | |||
| 15bbbf3cc4 | |||
| ebd62915a2 | |||
| 934dadbcdb | |||
| e75470f2e8 | |||
| 6de77049b3 | |||
| 34ecbf9204 | |||
| d9bd49fb8a | |||
| 5e8501002d | |||
| c582bd3f2d | |||
| 15c7110647 | |||
| c2109496a7 | |||
| cede5ddc9d | |||
| 385428f6ea | |||
| f973d151c6 | |||
| e60595ccc3 | |||
| 7b06ec2ae6 | |||
| 12947fd616 | |||
| 189f2aafbb | |||
| 2efe6017bf | |||
| 3293a34219 | |||
| d2cd3fd0ed | |||
| 4ac874f6b4 | |||
| 1aefe9dc82 | |||
| 0857acfc50 | |||
| 5c3ddd58e1 | |||
| 7848a0090a | |||
| 86599a27bc | |||
| d9f58acbb7 | |||
| 42d4a5b207 | |||
| b22e2d678a | |||
| 26433e078e | |||
| bf1b916d50 | |||
| faf5b53321 | |||
| ac8028a21e | |||
| 96ef704da7 | |||
| 8d01d881cb | |||
| 7d2f012387 | |||
| da7af42e49 | |||
| b1e7a5c46e | |||
| 7b4643208e | |||
| 2dd44a6368 | |||
| 19b2264e43 | |||
| a19ed91f61 | |||
| 04c2353096 | |||
| 6cae0cebe7 | |||
| ba901b5f55 | |||
| e6a60d6cba | |||
| d638e62811 | |||
| 18465ed4ef | |||
| f5672221a6 | |||
| 99c5efa680 | |||
| 1648a100e3 | |||
| d3a74f61e4 | |||
| 0c6ee69362 | |||
| aaf1061111 | |||
| 79c61f479a | |||
| 2a12dbd0ad | |||
| f0cac32351 | |||
| 5bc666676a | |||
| 2a528414ed | |||
| ecda0c61d9 | |||
| 399a40ca22 | |||
| e47133be1a | |||
| 5ce64a49a8 | |||
| 4c47955498 | |||
| fab2494f8e | |||
| 96a75ba908 | |||
| 3a41fe663a | |||
| 678af78347 | |||
| 9d8a523471 | |||
| b7cd0caff2 | |||
| cdc6b3bd68 | |||
| 7f0c5673b4 | |||
| 2a13cc7fe2 | |||
| 7c6038fe28 | |||
| 5925aecd5b | |||
| e6390c32e1 | |||
| 1590a5cc2b | |||
| 78d412d1d0 | |||
| d34a756929 | |||
| f15a1974d5 | |||
| 08139a021a | |||
| fbe982f47b | |||
| 5e6e978438 | |||
| b990a776b2 | |||
| 88cbf88756 | |||
| 373c411baa | |||
| 990e68804c | |||
| b295a57281 | |||
| 673ca37396 | |||
| 44beb5b778 | |||
| 00ac287223 | |||
| 09b53ccf9f | |||
| 0fee545400 | |||
| 14370fe9cf | |||
| 7316871e62 | |||
| 239121b0e1 | |||
| 26de11932a | |||
| 1f8b955a0f | |||
| b41b0ab95f | |||
| a8d1f2318e | |||
| dac7140410 | |||
| 0cf86c5c3b | |||
| d3ec77b0b4 | |||
| 814af739d0 | |||
| 68b75fc51e | |||
| 7ff3682aba | |||
| 91052ea0e1 | |||
| 20f38f3d8e | |||
| cdaf33529a | |||
| 6e37c0917c | |||
| 1db1ff9b91 | |||
| 3ba36ed4fc | |||
| cbe6f39030 | |||
| 6aa9abf046 | |||
| 9332886242 | |||
| c3e4ec630f | |||
| 65c8581db3 | |||
| 9136e13fdf | |||
| 9e5a3e288b | |||
| 2878d13c3d | |||
| 16ec6bc5c9 | |||
| 1d6d0cb5ba | |||
| 200ac08499 | |||
| 71649a2ac1 | |||
| fc852fed06 | |||
| 7222b29a88 | |||
| 2d3f483f3a | |||
| b4dbc18a14 | |||
| 8d73b7b679 | |||
| b640bbc20e | |||
| 9d0ab7a849 | |||
| 6f3d863ecd | |||
| ab6c97fef8 | |||
| a51205e302 | |||
| 64f8b75551 | |||
| 497b906121 | |||
| 04ba07e706 | |||
| ad1c970cdd | |||
| 7a7b391656 | |||
| 688631b6bd | |||
| 9677a3bd78 | |||
| 4d585dbcbc | |||
| f88ef758ca | |||
| c4f46c51b2 | |||
| 6b8bb279d4 | |||
| c51b96879a | |||
| 524cffa19c | |||
| 549c12cd1b | |||
| f6664f5466 | |||
| 151b07462c | |||
| 3506c2561b | |||
| 5bead81598 | |||
| 01ae511879 | |||
| 8ccaacb919 | |||
| 38c98cf9f7 | |||
| 737c8499eb | |||
| 12251b663a | |||
| ed008e2bf3 | |||
| 024465323b | |||
| 32381eb182 | |||
| 3f4d1e4fcc | |||
| 50a8d1abdb | |||
| 185c501809 | |||
| eb088bccfa | |||
| d0585de42d | |||
| e517a83541 | |||
| c7b9a77782 | |||
| 07dc300d5b | |||
| 59d3c4dd66 | |||
| d6712e2a10 | |||
| be263a6fa1 | |||
| f1cb143bd7 | |||
| ecbd6362f7 | |||
| fbfadc65da | |||
| 334f32f5d8 | |||
| 52c61da75d | |||
| 08a11c445c | |||
| 14b15022eb | |||
| 340d225039 | |||
| a3d8c8948e | |||
| 1b1989f794 | |||
| d8a4be7e98 | |||
| b942e08f58 | |||
| 8eb46987ff | |||
| 46596218ba | |||
| 202ea33690 | |||
| 23821bad24 | |||
| 49940935a9 | |||
| 14da2ebbd9 | |||
| 29dbf8d084 | |||
| b67bc26763 | |||
| 943c739521 | |||
| 85703712df | |||
| 88b3b3eeea | |||
| ab9011c825 | |||
| 91535cc086 | |||
| b135a43d97 | |||
| 0b8a9a503b | |||
| bf00f88be4 | |||
| 58d2b6838d | |||
| 9832c3de4f | |||
| 24b8789c79 | |||
| 01dd14bfae | |||
| bb04e93527 | |||
| 244aa67371 | |||
| dd607aa07d | |||
| 79d73d6ed4 | |||
| c48247ddb0 | |||
| 22707891bd | |||
| 962d8b6719 | |||
| 0f3bdb34b3 | |||
| c2921c1a8f | |||
| a19beda327 | |||
| b7775991bf | |||
| eedd73e522 | |||
| 50e2c21dbd | |||
| 54c3fb13f3 | |||
| 7f3bd61292 | |||
| dec4a0884a | |||
| 4edc8ab86a | |||
| b3a14cb48d | |||
| f47322cbee | |||
| a08065f282 | |||
| a33bec1ca4 | |||
| ca3d8a3816 | |||
| 2360ff9b0f | |||
| 0976fd804d | |||
| 2504441a4e | |||
| 0b5ccb31e1 | |||
| b91ea3e1da | |||
| 1588f7041a | |||
| bcf152e6f7 |
@@ -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
|
||||
@@ -27,6 +27,10 @@ add_panda_targets() {
|
||||
panda_h7_remote_can_ignition_only
|
||||
panda_hkg_remote_can_ignition_only
|
||||
panda_h7_hkg_remote_can_ignition_only
|
||||
panda_tesla_wake
|
||||
panda_h7_tesla_wake
|
||||
panda_tesla_wake_can_ignition_only
|
||||
panda_h7_tesla_wake_can_ignition_only
|
||||
panda_jungle_h7
|
||||
body_h7
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ using Car = import "car.capnp";
|
||||
|
||||
struct StarPilotCarControl @0x81c2f05a394cf4af {
|
||||
hudControl @0 :HUDControl;
|
||||
steeringLimitInfo @1 :SteeringLimitInfo;
|
||||
|
||||
struct HUDControl {
|
||||
audibleAlert @0 :AudibleAlert;
|
||||
@@ -49,6 +50,16 @@ struct StarPilotCarControl @0x81c2f05a394cf4af {
|
||||
uwu @22;
|
||||
}
|
||||
}
|
||||
|
||||
struct SteeringLimitInfo {
|
||||
valid @0 :Bool;
|
||||
modelLimitErrorDeg @1 :Float32;
|
||||
resumeLimitErrorDeg @2 :Float32;
|
||||
cooperativeLimitErrorDeg @3 :Float32;
|
||||
cooperativeOffsetDeg @4 :Float32;
|
||||
monoTime @5 :UInt64;
|
||||
combinedLimitErrorDeg @6 :Float32;
|
||||
}
|
||||
}
|
||||
|
||||
struct StarPilotCarParams @0xaedffd8f31e7b55d {
|
||||
@@ -226,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.
@@ -152,7 +152,8 @@ class FrequencyTracker:
|
||||
class SubMaster:
|
||||
def __init__(self, services: List[str], poll: Optional[str] = None,
|
||||
ignore_alive: Optional[List[str]] = None, ignore_avg_freq: Optional[List[str]] = None,
|
||||
ignore_valid: Optional[List[str]] = None, addr: str = "127.0.0.1", frequency: Optional[float] = None):
|
||||
ignore_valid: Optional[list[str]] = None, addr: str = "127.0.0.1", frequency: Optional[float] = None,
|
||||
drain_services: list[str] | None = None):
|
||||
self.frame = -1
|
||||
self.services = services
|
||||
self.seen = {s: False for s in services}
|
||||
@@ -160,6 +161,9 @@ class SubMaster:
|
||||
self.recv_time = {s: 0. for s in services}
|
||||
self.recv_frame = {s: 0 for s in services}
|
||||
self.sock = {}
|
||||
self.drained = {s: [] for s in (drain_services or [])}
|
||||
if not self.drained.keys() <= set(services):
|
||||
raise ValueError("Drained services must be subscribed")
|
||||
self.data = {}
|
||||
self.logMonoTime = {s: 0 for s in services}
|
||||
|
||||
@@ -187,7 +191,7 @@ class SubMaster:
|
||||
|
||||
for s in services:
|
||||
p = self.poller if s not in self.non_polled_services else None
|
||||
self.sock[s] = sub_sock(s, poller=p, addr=addr, conflate=True)
|
||||
self.sock[s] = sub_sock(s, poller=p, addr=addr, conflate=s not in self.drained)
|
||||
|
||||
try:
|
||||
data = new_message(s)
|
||||
@@ -207,14 +211,28 @@ class SubMaster:
|
||||
def _check_avg_freq(self, s: str) -> bool:
|
||||
return SERVICE_LIST[s].frequency > 0.99 and (s not in self.ignore_average_freq) and (s not in self.ignore_alive)
|
||||
|
||||
def _recv_socket(self, sock):
|
||||
message = recv_one_or_none(sock)
|
||||
if not self.drained or message is None:
|
||||
return message
|
||||
# Native Poller returns fresh socket wrappers; identify the service by data.
|
||||
service = message.which()
|
||||
if service not in self.drained:
|
||||
return message
|
||||
# Preserve event edges for observers, but update state/frequency only once.
|
||||
self.drained[service] = [message, *drain_sock(sock)]
|
||||
return self.drained[service][-1]
|
||||
|
||||
def update(self, timeout: int = 100) -> None:
|
||||
for service in self.drained:
|
||||
self.drained[service] = []
|
||||
msgs = []
|
||||
for sock in self.poller.poll(timeout):
|
||||
msgs.append(recv_one_or_none(sock))
|
||||
msgs.append(self._recv_socket(sock))
|
||||
|
||||
# non-blocking receive for non-polled sockets
|
||||
for s in self.non_polled_services:
|
||||
msgs.append(recv_one_or_none(self.sock[s]))
|
||||
msgs.append(self._recv_socket(self.sock[s]))
|
||||
self.update_msgs(time.monotonic(), msgs)
|
||||
|
||||
def update_msgs(self, cur_time: float, msgs: List[capnp.lib.capnp._DynamicStructReader]) -> None:
|
||||
@@ -262,6 +280,7 @@ class SubMaster:
|
||||
ignore_valid=self.ignore_valid,
|
||||
addr=self.addr,
|
||||
frequency=None if self.poll is not None else self.update_freq,
|
||||
drain_services=list(self.drained),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
import time
|
||||
import pytest
|
||||
from typing import Sized, cast
|
||||
|
||||
import cereal.messaging as messaging
|
||||
@@ -16,6 +17,29 @@ class TestSubMaster:
|
||||
# sleep to prevent multiple publishers error between tests
|
||||
zmq_sleep(3)
|
||||
|
||||
@pytest.mark.parametrize("poll", [None, "deviceState"])
|
||||
def test_drain_preserves_short_events_with_native_socket_wrappers(self, poll):
|
||||
pub = messaging.PubMaster(["carState", "deviceState"])
|
||||
sm = messaging.SubMaster(["carState", "deviceState"], poll=poll, drain_services=["carState"])
|
||||
zmq_sleep()
|
||||
pressed = messaging.new_message("carState", valid=True)
|
||||
button = pressed.carState.init("buttonEvents", 1)[0]
|
||||
button.type, button.pressed = "accelCruise", True
|
||||
pub.send("carState", pressed)
|
||||
latest = messaging.new_message("carState", valid=True)
|
||||
latest.carState.vEgo = 12.0
|
||||
pub.send("carState", latest)
|
||||
pub.send("deviceState", messaging.new_message("deviceState", valid=True))
|
||||
sm.update(1000)
|
||||
assert len(sm.drained["carState"]) == 2
|
||||
assert sm.drained["carState"][0].carState.buttonEvents[0].pressed
|
||||
assert sm["carState"].vEgo == 12.0 and not sm["carState"].buttonEvents
|
||||
assert sm.logMonoTime["carState"] == latest.logMonoTime
|
||||
assert sm.frame == 0 and all(sm.updated.values())
|
||||
sm.update(0)
|
||||
assert sm.drained["carState"] == []
|
||||
assert sm.frame == 1 and not any(sm.updated.values())
|
||||
|
||||
def test_init(self):
|
||||
sm = messaging.SubMaster(events)
|
||||
for p in [sm.updated, sm.recv_time, sm.recv_frame, sm.alive,
|
||||
|
||||
Binary file not shown.
+30
-3
@@ -18,6 +18,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"BootCount", {PERSISTENT, INT}},
|
||||
{"BluetoothAudioAddress", {PERSISTENT, STRING}},
|
||||
{"BluetoothAudioTestActive", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}},
|
||||
{"BluetoothDisconnectControllersOffroad", {PERSISTENT, BOOL, "0"}},
|
||||
{"BluetoothEnabled", {PERSISTENT, BOOL, "0"}},
|
||||
{"CalibrationParams", {PERSISTENT, BYTES}},
|
||||
{"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
@@ -109,6 +110,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LocationFilterInitialState", {PERSISTENT, BYTES}},
|
||||
{"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"LongitudinalPersonality", {PERSISTENT, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}},
|
||||
{"LongitudinalPersonalityProfiles", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
|
||||
{"NetworkMetered", {PERSISTENT, BOOL}},
|
||||
{"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
@@ -196,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}},
|
||||
@@ -316,7 +318,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DeveloperSidebarMetric7", {PERSISTENT, INT, "7", "0", 3}},
|
||||
{"DeveloperUI", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"GalaxyDeveloperMode", {PERSISTENT | DONT_LOG, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"GalaxyMobileDefault", {PERSISTENT | DONT_LOG, BOOL, "0", "0", 0, SETTINGS_ADVANCED}},
|
||||
{"GalaxyDeviceName", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
|
||||
{"GalaxyMobileDefault", {PERSISTENT | DONT_LOG, BOOL, "1", "1", 0, SETTINGS_SIMPLE}},
|
||||
{"DeveloperWidgets", {PERSISTENT, BOOL, "1", "0", 3}},
|
||||
{"DeviceManagement", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
|
||||
{"DeviceShutdown", {PERSISTENT, INT, "6", "6", 1, SETTINGS_SIMPLE}},
|
||||
@@ -350,6 +353,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DrivingModelVersion", {PERSISTENT, STRING, "v15", "v15", 1}},
|
||||
{"DynamicPathWidth", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"DynamicPedalsOnUI", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
|
||||
{"GpuModelReadySound", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"EngageVolume", {PERSISTENT, INT, "101", "101", 2, SETTINGS_SIMPLE}},
|
||||
{"EVTuning", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"Fahrenheit", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
@@ -360,6 +364,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IgnoreIgnitionLine", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"LongPitch", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"RemoteStartBootsComma", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"TeslaWakeOnCAN", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"RemapCancelToDistance", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"NAPAdaptiveAccel", {PERSISTENT, BOOL, "1", "1", 0, SETTINGS_SIMPLE}},
|
||||
{"NAPFollowDistance", {PERSISTENT, INT, "4", "4"}},
|
||||
@@ -384,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.
|
||||
@@ -440,6 +446,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IncreaseFollowingRain", {PERSISTENT, FLOAT, "0.0", "0.0", 2, SETTINGS_SIMPLE}},
|
||||
{"IncreaseFollowingRainStorm", {PERSISTENT, FLOAT, "0.0", "0.0", 2, SETTINGS_SIMPLE}},
|
||||
{"IncreaseFollowingSnow", {PERSISTENT, FLOAT, "0.0", "0.0", 2, SETTINGS_SIMPLE}},
|
||||
{"AggressiveCoolingEnabled", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"IncreaseThermalLimits", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"IssueReported", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
|
||||
{"KonikDongleId", {PERSISTENT, STRING, "", "", 0}},
|
||||
@@ -463,7 +470,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LeadDepartingAlert", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"LeadDetectionThreshold", {PERSISTENT, INT, "35", "50", 3}},
|
||||
{"LeadIndicator", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"LeadInfo", {PERSISTENT, BOOL, "1", "0", 3}},
|
||||
{"LeadInfo", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"LeadInfoMode", {PERSISTENT, INT, "2", "2", 3}},
|
||||
{"LKASButtonControl", {PERSISTENT, INT, "5", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"LockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
|
||||
{"LockDoorsTimer", {PERSISTENT, INT, "0", "0", 0}},
|
||||
@@ -608,6 +616,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"RelaxedJerkDeceleration", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||
{"RelaxedJerkSpeed", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||
{"RelaxedJerkSpeedDecrease", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||
{"ReverseCruise", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
|
||||
{"RivianAngleControl", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"RivianAngleSaturated", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL, "0", "0"}},
|
||||
{"RivianToiRecoveryFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL, "0", "0"}},
|
||||
@@ -617,6 +626,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"RotatingWheel", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
|
||||
{"ScreenBrightness", {PERSISTENT, INT, "101", "101", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenBrightnessOnroad", {PERSISTENT, INT, "101", "101", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenBrightnessManual", {PERSISTENT, INT, "100", "100", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenBrightnessOffset", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenBrightnessOnroadManual", {PERSISTENT, INT, "100", "100", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenBrightnessOnroadOffset", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenManagement", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
|
||||
{"ScreenRecorder", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ScreenTimeout", {PERSISTENT, INT, "30", "30", 2, SETTINGS_SIMPLE}},
|
||||
@@ -626,6 +639,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SafeModeBackup", {PERSISTENT, JSON, "{}", "{}"}},
|
||||
{"SetSpeedLimit", {PERSISTENT, BOOL, "0", "0", 1}},
|
||||
{"SetSpeedOffset", {PERSISTENT, FLOAT, "0.0", "0.0", 2, SETTINGS_SIMPLE}},
|
||||
{"ShowBrakeStatus", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ShowCEMStatus", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ShowCCMStatus", {PERSISTENT, BOOL, "0", "0", 2}},
|
||||
{"ShowCPU", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
@@ -687,6 +701,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"StandardJerkSpeed", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||
{"StandardJerkSpeedDecrease", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||
{"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}},
|
||||
{"StandbyWakeWarningAlert", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
|
||||
{"StandbyWakeCriticalAlert", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
|
||||
{"StandbyWakeTurnSignal", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"StartAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
|
||||
{"StartAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
|
||||
{"StartupMessageBottom", {PERSISTENT, STRING, "Always keep hands on wheel and eyes on road", "Always keep hands on wheel and eyes on road", 0}},
|
||||
@@ -706,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}},
|
||||
@@ -719,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.
@@ -5,7 +5,7 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName
|
||||
from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName
|
||||
|
||||
class TestParams:
|
||||
def setup_method(self):
|
||||
@@ -128,6 +128,31 @@ class TestParams:
|
||||
assert self.params.get("LiveParameters") is None
|
||||
assert self.params.get("LiveParameters", return_default=True) is None
|
||||
|
||||
def test_longitudinal_personality_profiles_json_round_trip(self):
|
||||
key = "LongitudinalPersonalityProfiles"
|
||||
value = {
|
||||
"schemaVersion": 1,
|
||||
"enabled": False,
|
||||
"axes": {
|
||||
"acceleration": {
|
||||
"speed": {"unit": "mph", "values": [0.0, 11.184681, 22.369363, 33.554044, 44.738726, 55.923407, 89.477452]},
|
||||
"value": {"unit": "m/s^2", "meaning": "maximum_requested_acceleration"},
|
||||
},
|
||||
"braking": {
|
||||
"speed": {"unit": "mph", "values": [0.0, 11.184681, 22.369363, 33.554044, 44.738726, 55.923407, 89.477452]},
|
||||
"value": {"unit": "m/s^2", "meaning": "cruise_slc_deceleration_magnitude"},
|
||||
},
|
||||
"following": {"speed": {"unit": "mph", "values": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]}, "value": {"unit": "s", "meaning": "base_time_headway"}},
|
||||
},
|
||||
"profiles": {},
|
||||
}
|
||||
self.params.remove(key)
|
||||
|
||||
assert self.params.get_type(key) == ParamKeyType.JSON
|
||||
assert self.params.get(key) is None
|
||||
self.params.put(key, value)
|
||||
assert self.params.get(key) == value
|
||||
|
||||
def test_params_get_type(self):
|
||||
# json
|
||||
self.params.put("ApiCache_DriveStats", {"a": 0})
|
||||
|
||||
+2
-1
@@ -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> |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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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>|||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Custom personality graphs
|
||||
|
||||
Each personality keeps its own Custom acceleration, braking and following curve.
|
||||
Selecting a named preset changes the active selection without deleting Custom
|
||||
points. Selecting Custom again restores those points, including after a reload
|
||||
or restart. If a category has never had Custom points, it is initialized from
|
||||
the current selection, as before.
|
||||
|
||||
The existing **Reset to default** button, below each Custom graph's numeric
|
||||
points in New Galaxy's Advanced section, replaces only that category's Custom
|
||||
curve. It leaves the category set to Custom. The server resolves the reset
|
||||
values; the dashed **Dom default** line uses the same resolver.
|
||||
|
||||
Defaults are Dom's configured base curves sampled at the editor's 10 mph
|
||||
points. They include Traffic's dedicated acceleration and braking, following
|
||||
settings, global tuning switches and powertrain overrides. Where gear mapping
|
||||
is enabled, the reference uses normal gear. Live Eco/Sport gear, weather,
|
||||
lead/stop and overspeed adjustments remain on the existing controller paths.
|
||||
Sampling cannot reproduce every native breakpoint or between-point value;
|
||||
resetting a Custom graph is not the same as delegating to the Dom-default
|
||||
runtime path.
|
||||
|
||||
Dom-default points outside the ordinary editor range (such as Traffic braking
|
||||
at 0.35 m/s², configured Traffic following at 0.5 seconds or truck acceleration
|
||||
at 6 m/s²) remain visible and are preserved when another point is edited.
|
||||
New point edits still use the existing authoring bounds. This does not expand
|
||||
braking authority or change acceleration/braking preset definitions.
|
||||
|
||||
## Following presets
|
||||
|
||||
Named following presets now match Dom's factory following settings with custom
|
||||
personalities enabled. Close follows Aggressive, Medium follows Standard and
|
||||
Far follows Relaxed. The presets are available in every personality.
|
||||
|
||||
| Preset | Previous curve | Revised curve |
|
||||
| --- | --- | --- |
|
||||
| Close | 1.25 s at every speed | 1.25 s through 45 mph, falling to 1.0 s at 70 mph |
|
||||
| Medium | 1.45 s at every speed | 1.45 s through 45 mph, falling to 1.2 s at 70 mph |
|
||||
| Far | 1.75 s at every speed | 1.6 s through 45 mph, falling to 1.4 s at 70 mph |
|
||||
| Traffic | No named preset | 0.75 s at rest, rising to 1.6 s at 25 m/s (55.92 mph) |
|
||||
|
||||
Interpolation is linear between the stated breakpoints and constant outside
|
||||
them. Named presets use the exact native speed axes at runtime. First-use
|
||||
Custom conversion samples them onto the existing 10 mph editor grid.
|
||||
|
||||
Existing v1/v2 Close, Medium and Far selections keep their old fixed headways
|
||||
as `legacy_close`, `legacy_medium` and `legacy_far`. Both Galaxy pickers show
|
||||
the selected compatibility entry as **Previous Close**, **Previous Medium**
|
||||
or **Previous Far**. Explicitly selecting a current preset adopts its new curve.
|
||||
The previous entry disappears when it is no longer selected.
|
||||
|
||||
Existing `dom_default` selections continue to inherit configured settings;
|
||||
they are not silently converted to fixed named presets. Fresh profiles also
|
||||
retain this inheritance. The named curves match untouched factory settings;
|
||||
users' changed global following values can still differ from them.
|
||||
|
||||
Acceleration and braking presets are unchanged. Standard acceleration and Eco
|
||||
braking match the normal factory defaults for Aggressive, Standard and Relaxed
|
||||
when named-preset and global powertrain tuning agree. Named presets use detected
|
||||
EV/truck tuning; the Dom-default resolver respects the global tuning switches,
|
||||
so these can differ. Traffic retains its dedicated acceleration/braking defaults;
|
||||
this change adds only its named following preset.
|
||||
|
||||
## Storage compatibility
|
||||
|
||||
Profile document version 3 retains `curve` and optional `legacyCurve` while
|
||||
`preset` is a named preset or `dom_default`. These retained values are dormant;
|
||||
only Custom uses them. An actual graph edit or reset retires preserved v1
|
||||
interpolation for that category; a preset switch or unchanged submission does
|
||||
not.
|
||||
|
||||
Valid v2 documents retain their runtime meaning and are upgraded on the next
|
||||
normal write, including the fixed following compatibility names above.
|
||||
Version 1 keeps its existing explicit, verified migration flow. Reads never
|
||||
rewrite Params. Category conflict detection, off-road checks and atomic profile
|
||||
document writes still apply to edits and resets.
|
||||
|
||||
Older builds do not understand v3 documents. Retain a compatible settings
|
||||
backup before rolling back to one of those builds. Curves discarded before
|
||||
this change cannot be recovered automatically.
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
include opendbc/car/car.capnp
|
||||
include opendbc/car/include/c++.capnp
|
||||
include opendbc/dbc/hyundai_kia_ray_pedal.dbc
|
||||
recursive-include opendbc/safety *.h
|
||||
|
||||
@@ -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)|
|
||||
|
||||
@@ -13,7 +13,7 @@ from opendbc.car.subaru.subarucan import subaru_checksum
|
||||
from opendbc.car.chrysler.chryslercan import chrysler_checksum, fca_giorgio_checksum
|
||||
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
||||
from opendbc.car.volkswagen.mlbcan import volkswagen_mlb_checksum
|
||||
from opendbc.car.volkswagen.mqbcan import volkswagen_mqb_meb_checksum, xor_checksum
|
||||
from opendbc.car.volkswagen.mqbcan import volkswagen_meb_alt_crc_checksum, volkswagen_mqb_meb_checksum, xor_checksum
|
||||
from opendbc.car.tesla.teslacan import tesla_checksum
|
||||
from opendbc.car.body.bodycan import body_checksum
|
||||
from opendbc.car.psa.psacan import psa_checksum
|
||||
@@ -194,8 +194,10 @@ def get_checksum_state(dbc_name: str) -> ChecksumState | None:
|
||||
return ChecksumState(4, 2, 3, 5, False, SignalType.HONDA_CHECKSUM, honda_checksum)
|
||||
elif dbc_name.startswith(("toyota_", "lexus_")):
|
||||
return ChecksumState(8, -1, 7, -1, False, SignalType.TOYOTA_CHECKSUM, toyota_checksum)
|
||||
elif dbc_name.startswith("hyundai_canfd_generated"):
|
||||
elif dbc_name.startswith(("hyundai_canfd_generated", "hyundai_radar_210_21f_generated")):
|
||||
return ChecksumState(16, -1, 0, -1, True, SignalType.HKG_CAN_FD_CHECKSUM, hkg_can_fd_checksum)
|
||||
elif dbc_name.startswith("vw_meb_2024"):
|
||||
return ChecksumState(8, 4, 0, 0, True, SignalType.VOLKSWAGEN_MQB_MEB_CHECKSUM, volkswagen_meb_alt_crc_checksum)
|
||||
elif dbc_name.startswith(("vw_mqb", "vw_mqbevo", "vw_meb")):
|
||||
return ChecksumState(8, 4, 0, 0, True, SignalType.VOLKSWAGEN_MQB_MEB_CHECKSUM, volkswagen_mqb_meb_checksum)
|
||||
elif dbc_name.startswith("vw_mlb"):
|
||||
|
||||
@@ -90,6 +90,7 @@ class Bus(StrEnum):
|
||||
main = auto()
|
||||
party = auto()
|
||||
ap_party = auto()
|
||||
ap_pt = auto()
|
||||
|
||||
|
||||
def rate_limit(new_value, last_value, dw_step, up_step):
|
||||
|
||||
@@ -56,6 +56,20 @@ GM_CANDIDATE_PREFIXES = ("CHEVROLET_", "GMC_", "CADILLAC_", "BUICK_", "HOLDEN_")
|
||||
GM_CORE_FINGERPRINT_MSGS = frozenset((190, 201, 209, 211, 241))
|
||||
GM_CAMERA_BUS = 2
|
||||
GM_VOLT_CAMERA_MSG = 0x320
|
||||
GM_SUBURBAN_CAMERA_VIN_PREFIX = "1GNSKJKJ"
|
||||
GM_SUBURBAN_CAMERA_PT_SIGNATURE = {
|
||||
190: 6,
|
||||
201: 8,
|
||||
209: 7,
|
||||
211: 2,
|
||||
241: 6,
|
||||
304: 1,
|
||||
320: 3,
|
||||
}
|
||||
GM_CAMERA_DIAGNOSTIC_MESSAGES = {
|
||||
0x24b: 8,
|
||||
0x64b: 8,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_forced_candidate(candidate: str | None) -> str | None:
|
||||
@@ -152,6 +166,24 @@ def _normalize_gm_volt_candidate(candidate: str | None, fingerprints: dict[int,
|
||||
return candidate
|
||||
|
||||
|
||||
def _normalize_gm_suburban_camera_candidate(candidate: str | None, fingerprints: dict[int, dict], vin: str | None) -> str | None:
|
||||
"""Resolve the 2019 Suburban camera-harness variant when CAN is shared with Yukon."""
|
||||
if candidate not in (None, "GMC_YUKON", "GMC_YUKON_CC"):
|
||||
return candidate
|
||||
|
||||
if not isinstance(vin, str) or not vin.startswith(GM_SUBURBAN_CAMERA_VIN_PREFIX):
|
||||
return candidate
|
||||
|
||||
powertrain = fingerprints.get(0, {})
|
||||
camera = fingerprints.get(GM_CAMERA_BUS, {})
|
||||
if not all(powertrain.get(address) == length for address, length in GM_SUBURBAN_CAMERA_PT_SIGNATURE.items()):
|
||||
return candidate
|
||||
if not all(camera.get(address) == length for address, length in GM_CAMERA_DIAGNOSTIC_MESSAGES.items()):
|
||||
return candidate
|
||||
|
||||
return "CHEVROLET_SUBURBAN_CAMERA"
|
||||
|
||||
|
||||
def _is_gm_candidate(candidate: str | None) -> bool:
|
||||
return isinstance(candidate, str) and candidate.startswith(GM_CANDIDATE_PREFIXES)
|
||||
|
||||
@@ -307,6 +339,10 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
|
||||
stored_candidate = _normalize_forced_candidate(params.get("CarModel"))
|
||||
cached_candidate = _normalize_forced_candidate(getattr(cached_params, "carFingerprint", None))
|
||||
|
||||
if candidate is None and stored_candidate is None and cached_candidate is None:
|
||||
candidate = _normalize_gm_suburban_camera_candidate(candidate, fingerprints, vin)
|
||||
fingerprinted_candidate = candidate
|
||||
|
||||
if candidate is None:
|
||||
gm_fallback_candidate = _get_gm_stored_candidate_fallback(fingerprints, stored_candidate, cached_candidate)
|
||||
if gm_fallback_candidate is not None:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -50,6 +50,7 @@ class FordSafetyFlags(IntFlag):
|
||||
LONG_CONTROL = 1
|
||||
CANFD = 2
|
||||
LKA_STEERING = 4
|
||||
MACH_E_CURVATURE = 8
|
||||
|
||||
|
||||
class FordFlags(IntFlag):
|
||||
|
||||
@@ -6,7 +6,7 @@ from opendbc.car.lateral import apply_driver_steer_torque_limits
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.gm.values import (
|
||||
ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, SDGM_CAR, AccState, CanBus, CarControllerParams,
|
||||
ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, GM_AUTO_HOLD_CARS, SDGM_CAR, AccState, CanBus, CarControllerParams,
|
||||
CruiseButtons, GMFlags, GMSafetyFlags,
|
||||
)
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
@@ -309,7 +309,7 @@ def supports_volt_auto_hold(CP, auto_hold_enabled: bool):
|
||||
auto_hold_enabled and
|
||||
getattr(CP, "openpilotLongitudinalControl", False) and
|
||||
stock_hold_safety_ready and
|
||||
CP.carFingerprint in AUTO_HOLD_VOLT_CARS
|
||||
CP.carFingerprint in GM_AUTO_HOLD_CARS
|
||||
)
|
||||
|
||||
|
||||
@@ -852,7 +852,6 @@ class CarController(CarControllerBase):
|
||||
CAR.CHEVROLET_VOLT_CC,
|
||||
CAR.CHEVROLET_MALIBU_CC,
|
||||
CAR.CHEVROLET_MALIBU_HYBRID_CC,
|
||||
CAR.BUICK_LACROSSE,
|
||||
}
|
||||
|
||||
if (self.CP.enableGasInterceptorDEPRECATED and self.CP.carFingerprint in CC_REGEN_PADDLE_CAR and
|
||||
@@ -1160,7 +1159,13 @@ class CarController(CarControllerBase):
|
||||
if should_send_cc_button_spam(self.CP, CC, CS):
|
||||
if self.CP.carFingerprint != CAR.CADILLAC_XT4_CC:
|
||||
# Using extend instead of append since the message is only sent intermittently
|
||||
can_sends.extend(gmcan.create_gm_cc_spam_command(self.packer_pt, self, CS, actuators, starpilot_toggles))
|
||||
longitudinal_adjustment_active = bool(getattr(
|
||||
CS, "openpilot_longitudinal_adjustment_active", CC.hudControl.leadVisible,
|
||||
))
|
||||
can_sends.extend(gmcan.create_gm_cc_spam_command(
|
||||
self.packer_pt, self, CS, actuators, starpilot_toggles,
|
||||
longitudinal_adjustment_active=longitudinal_adjustment_active,
|
||||
))
|
||||
else:
|
||||
if (CS.out.cruiseState.enabled and CC.enabled and self.frame % 52 == 0 and
|
||||
CS.cruise_buttons == CruiseButtons.UNPRESS and CS.out.gasPressed and CS.out.cruiseState.speed < CS.out.vEgo < hud_v_cruise):
|
||||
@@ -1192,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)
|
||||
|
||||
@@ -18,6 +18,7 @@ from opendbc.car.gm.values import (
|
||||
AccState,
|
||||
CanBus,
|
||||
CruiseButtons,
|
||||
GM_AUTO_HOLD_CARS,
|
||||
GMFlags,
|
||||
SDGM_CAR,
|
||||
STEER_THRESHOLD,
|
||||
@@ -32,6 +33,7 @@ STANDSTILL_THRESHOLD = 10 * 0.0311
|
||||
VOLT_EBCM_BRAKE_PRESSED_THRESHOLD = 6 / 0xd0
|
||||
AUTO_HOLD_MIN_DRIVE_TIME_S = 3.0
|
||||
AUTO_HOLD_REGEN_RELEASE_COOLDOWN_S = 1.0
|
||||
ACC_STARTUP_FAULT_GRACE_PERIOD_S = 5.0
|
||||
|
||||
BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
|
||||
CruiseButtons.MAIN: ButtonType.mainCruise, CruiseButtons.CANCEL: ButtonType.cancel}
|
||||
@@ -67,6 +69,36 @@ def update_auto_hold_drive_timers(in_drive_for_hold: bool, moving_for_hold: bool
|
||||
return auto_hold_drive_time, one_pedal_drive_time
|
||||
|
||||
|
||||
def is_gm_auto_hold_active(car_fingerprint: str, auto_hold_engaged: bool, in_drive_for_hold: bool,
|
||||
cruise_available: bool, standstill: bool, gas_pressed: bool) -> bool:
|
||||
return (
|
||||
auto_hold_engaged and
|
||||
car_fingerprint in GM_AUTO_HOLD_CARS and
|
||||
in_drive_for_hold and
|
||||
cruise_available and
|
||||
standstill and
|
||||
not gas_pressed
|
||||
)
|
||||
|
||||
|
||||
def update_startup_acc_fault_suppression(car_fingerprint: str, system_power_mode: int,
|
||||
previous_system_power_mode: int, timer: float,
|
||||
acc_state: int, friction_brake_unavailable: bool) -> tuple[float, bool]:
|
||||
if car_fingerprint != CAR.BUICK_LACROSSE:
|
||||
return 0.0, False
|
||||
|
||||
if system_power_mode == 2 and previous_system_power_mode != 2:
|
||||
timer = ACC_STARTUP_FAULT_GRACE_PERIOD_S
|
||||
elif system_power_mode != 2:
|
||||
timer = 0.0
|
||||
|
||||
if timer <= 0.0 or acc_state != AccState.FAULTED:
|
||||
return 0.0, False
|
||||
|
||||
timer = max(timer - DT_CTRL, 0.0)
|
||||
return timer, timer > 0.0 and not friction_brake_unavailable
|
||||
|
||||
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP, FPCP):
|
||||
super().__init__(CP, FPCP)
|
||||
@@ -105,6 +137,8 @@ class CarState(CarStateBase):
|
||||
self.lkas_previously_enabled = 0
|
||||
self.lkas_enabled = 0
|
||||
self.pcm_acc_status = AccState.OFF
|
||||
self.system_power_mode = 0
|
||||
self.startup_acc_fault_suppression_timer = 0.0
|
||||
self.stock_fcw_alert = 0
|
||||
self.car_gps_config = get_car_gps_config(CP)
|
||||
self.car_gps_supported = self.car_gps_config is not None
|
||||
@@ -350,8 +384,18 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0
|
||||
ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1
|
||||
ret.accFaulted = (pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.FAULTED or
|
||||
pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
|
||||
acc_state = pt_cp.vl["AcceleratorPedal2"]["CruiseState"]
|
||||
friction_brake_unavailable = pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1
|
||||
self.startup_acc_fault_suppression_timer, suppress_startup_acc_fault = update_startup_acc_fault_suppression(
|
||||
self.CP.carFingerprint,
|
||||
int(pt_cp.vl["BCMGeneralPlatformStatus"]["SystemPowerMode"]),
|
||||
self.system_power_mode,
|
||||
self.startup_acc_fault_suppression_timer,
|
||||
acc_state,
|
||||
friction_brake_unavailable,
|
||||
)
|
||||
self.system_power_mode = int(pt_cp.vl["BCMGeneralPlatformStatus"]["SystemPowerMode"])
|
||||
ret.accFaulted = (acc_state == AccState.FAULTED and not suppress_startup_acc_fault) or friction_brake_unavailable
|
||||
|
||||
ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
|
||||
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
|
||||
@@ -400,6 +444,11 @@ class CarState(CarStateBase):
|
||||
self.auto_hold_fault_suppression_timer = max(self.auto_hold_fault_suppression_timer - DT_CTRL, 0.0)
|
||||
ret.accFaulted = False
|
||||
|
||||
ret.brakeHoldActive = is_gm_auto_hold_active(
|
||||
self.CP.carFingerprint, self.auto_hold_engaged, in_drive_for_hold,
|
||||
ret.cruiseState.available, ret.standstill, ret.gasPressed,
|
||||
)
|
||||
|
||||
if self.CP.enableBsm and not sdgm_non_volt:
|
||||
ret.leftBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
|
||||
ret.rightBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
|
||||
|
||||
@@ -212,6 +212,10 @@ FINGERPRINTS.update({
|
||||
CAR.CADILLAC_ESCALADE_ASCM: FINGERPRINTS[CAR.CADILLAC_ESCALADE],
|
||||
CAR.CADILLAC_ESCALADE_ESV_2019_ASCM: [{**fp, SASCM_ADDRESS: 8} for fp in FINGERPRINTS[CAR.CADILLAC_ESCALADE_ESV_2019]],
|
||||
CAR.CHEVROLET_SUBURBAN: FINGERPRINTS[CAR.CHEVROLET_SUBURBAN_CC],
|
||||
CAR.CHEVROLET_SUBURBAN_ASCM: FINGERPRINTS[CAR.CHEVROLET_SUBURBAN_CC],
|
||||
# The camera-harness Suburban shares the observed CAN map with the 2019 Yukon;
|
||||
# VIN and camera-bus diagnostics disambiguate it during live fingerprinting.
|
||||
CAR.CHEVROLET_SUBURBAN_CAMERA: FINGERPRINTS[CAR.GMC_YUKON],
|
||||
CAR.GMC_YUKON_CC: FINGERPRINTS[CAR.GMC_YUKON],
|
||||
CAR.CADILLAC_XT6: FINGERPRINTS[CAR.CHEVROLET_TRAVERSE],
|
||||
CAR.CADILLAC_XT5: FINGERPRINTS[CAR.CHEVROLET_TRAVERSE],
|
||||
|
||||
@@ -31,6 +31,8 @@ BOLT_CC_DIRECTION_MEMORY_S = 1.5
|
||||
VOLT_CC_CARS = {
|
||||
CAR.CHEVROLET_VOLT_CC,
|
||||
}
|
||||
VOLT_CC_FREE_REQUEST_DEADBAND_MPH = 5.0
|
||||
VOLT_CC_ACTIVE_REQUEST_DEADBAND_MPH = 2.0
|
||||
|
||||
|
||||
def malibu_phase_map_for_button(button):
|
||||
@@ -339,10 +341,35 @@ def stabilize_bolt_cc_button(controller, CP, requested_button):
|
||||
return requested_button
|
||||
|
||||
|
||||
def _create_volt_cc_spam_command(CS, actuators, ms_convert):
|
||||
def _create_volt_cc_spam_command(CS, actuators, ms_convert, longitudinal_adjustment_active):
|
||||
accel = float(actuators.accel)
|
||||
speed_setpoint = int(round(CS.out.cruiseState.speed * ms_convert))
|
||||
ego_speed = CS.out.vEgo * ms_convert
|
||||
requested_setpoint = (CS.out.vEgo * 1.01 + 3 * accel) * ms_convert
|
||||
deadband_mph = (
|
||||
VOLT_CC_ACTIVE_REQUEST_DEADBAND_MPH if longitudinal_adjustment_active
|
||||
else VOLT_CC_FREE_REQUEST_DEADBAND_MPH
|
||||
)
|
||||
request_deadband = deadband_mph * (CV.MPH_TO_KPH if ms_convert == CV.MS_TO_KPH else 1.0)
|
||||
|
||||
target_setpoint = None
|
||||
v_cruise_kph = float(getattr(CS.out, "vCruise", 0.0))
|
||||
if 0.0 < v_cruise_kph < 255.0:
|
||||
is_metric = ms_convert == CV.MS_TO_KPH
|
||||
target_setpoint = int(round(v_cruise_kph if is_metric else v_cruise_kph * CV.KPH_TO_MPH))
|
||||
|
||||
moving_toward_target = target_setpoint is not None and (
|
||||
(accel > 0.0 and speed_setpoint < target_setpoint) or
|
||||
(accel < 0.0 and speed_setpoint > target_setpoint)
|
||||
)
|
||||
if target_setpoint is not None and accel > 0.0 and speed_setpoint >= target_setpoint:
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
if (target_setpoint is not None and accel < 0.0 and speed_setpoint <= target_setpoint and
|
||||
not longitudinal_adjustment_active):
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
|
||||
if not moving_toward_target and abs(requested_setpoint - speed_setpoint) <= request_deadband:
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
|
||||
if accel == 0.0:
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
@@ -361,7 +388,7 @@ def _create_volt_cc_spam_command(CS, actuators, ms_convert):
|
||||
return CruiseButtons.RES_ACCEL, rate
|
||||
|
||||
|
||||
def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggles):
|
||||
def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggles, longitudinal_adjustment_active=False):
|
||||
accel = actuators.accel
|
||||
v_ego = CS.out.vEgo
|
||||
cruise_btn = CruiseButtons.INIT
|
||||
@@ -376,7 +403,7 @@ def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggl
|
||||
comparison_setpoint = projected_setpoint if bolt_cc else desired_setpoint
|
||||
|
||||
if CS.CP.carFingerprint in VOLT_CC_CARS:
|
||||
cruise_btn, rate = _create_volt_cc_spam_command(CS, actuators, ms_convert)
|
||||
cruise_btn, rate = _create_volt_cc_spam_command(CS, actuators, ms_convert, longitudinal_adjustment_active)
|
||||
else:
|
||||
if CS.CP.minEnableSpeed - (desired_setpoint / ms_convert) > 3.25:
|
||||
cruise_btn = CruiseButtons.CANCEL
|
||||
|
||||
@@ -15,6 +15,7 @@ from opendbc.car.gm.values import (
|
||||
CC_ONLY_CAR,
|
||||
CC_REGEN_PADDLE_CAR,
|
||||
EV_CAR,
|
||||
GM_AUTO_HOLD_CARS,
|
||||
SDGM_CAR,
|
||||
CarControllerParams,
|
||||
CanBus,
|
||||
@@ -305,7 +306,7 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.HW_CAM_LONG.value
|
||||
|
||||
elif is_camera_acc:
|
||||
ret.alphaLongitudinalAvailable = (candidate not in CC_ONLY_CAR) and not ret.enableGasInterceptorDEPRECATED
|
||||
ret.alphaLongitudinalAvailable = candidate not in (CC_ONLY_CAR | ALT_ACCS) and not ret.enableGasInterceptorDEPRECATED
|
||||
ret.networkLocation = NetworkLocation.fwdCamera
|
||||
ret.radarUnavailable = True
|
||||
ret.pcmCruise = not ret.enableGasInterceptorDEPRECATED
|
||||
@@ -408,7 +409,7 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.steerActuatorDelay = 0.1 # Default delay, not measured yet
|
||||
|
||||
ret.steerLimitTimer = 0.4
|
||||
ret.radarTimeStepDEPRECATED = 0.0667 # GM radar runs at 15Hz instead of the standard 20Hz
|
||||
ret.radarTimeStepDEPRECATED = 0.15 if candidate == CAR.BUICK_LACROSSE else 0.0667
|
||||
ret.longitudinalActuatorDelay = 0.5 # large delay to initially start braking
|
||||
|
||||
if candidate in (
|
||||
@@ -440,7 +441,7 @@ class CarInterface(CarInterfaceBase):
|
||||
elif candidate in (CAR.BUICK_LACROSSE, CAR.BUICK_LACROSSE_ASCM, CAR.BUICK_LACROSSE_ASCM_19US):
|
||||
CarInterfaceBase.configure_torque_tune(CAR.BUICK_LACROSSE, ret.lateralTuning)
|
||||
if candidate == CAR.BUICK_LACROSSE_ASCM_19US:
|
||||
ret.minSteerSpeed = 27 * CV.MPH_TO_MS
|
||||
ret.minSteerSpeed = 28 * CV.MPH_TO_MS
|
||||
|
||||
elif candidate == CAR.CADILLAC_ESCALADE:
|
||||
ret.minEnableSpeed = -1. # engage speed is decided by pcm
|
||||
@@ -521,7 +522,7 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.steerActuatorDelay = 0.2
|
||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
|
||||
|
||||
elif candidate in (CAR.CHEVROLET_SUBURBAN, CAR.CHEVROLET_SUBURBAN_CC):
|
||||
elif candidate in (CAR.CHEVROLET_SUBURBAN, CAR.CHEVROLET_SUBURBAN_ASCM, CAR.CHEVROLET_SUBURBAN_CAMERA, CAR.CHEVROLET_SUBURBAN_CC):
|
||||
ret.steerActuatorDelay = 0.2
|
||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
|
||||
|
||||
@@ -710,18 +711,19 @@ class CarInterface(CarInterfaceBase):
|
||||
if remote_start_boots_comma:
|
||||
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_REMOTE_START_BOOTS_COMMA.value
|
||||
|
||||
volt_stock_friction_brake_safety = (
|
||||
gm_stock_friction_brake_safety = (
|
||||
ret.openpilotLongitudinalControl and
|
||||
(gm_auto_hold or volt_one_pedal_mode) and
|
||||
candidate in {
|
||||
CAR.CHEVROLET_VOLT,
|
||||
CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
}
|
||||
(
|
||||
(gm_auto_hold and candidate in GM_AUTO_HOLD_CARS) or
|
||||
(volt_one_pedal_mode and candidate in {
|
||||
CAR.CHEVROLET_VOLT,
|
||||
CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
})
|
||||
)
|
||||
)
|
||||
if volt_stock_friction_brake_safety:
|
||||
# Reuse the paddle-scheduler safety bit as a Volt stock friction-brake
|
||||
if gm_stock_friction_brake_safety:
|
||||
# marker on non-pedal paths. Auto hold and one-pedal can run while OP
|
||||
# longitudinal is configured but not currently active, so the bit must
|
||||
# be present regardless of the current long-control mode. Do not expose
|
||||
|
||||
@@ -431,6 +431,15 @@ def test_volt_auto_hold_requires_toggle_supported_non_cc_only_volt_and_stock_saf
|
||||
),
|
||||
True,
|
||||
)
|
||||
assert supports_volt_auto_hold(
|
||||
SimpleNamespace(
|
||||
carFingerprint=CAR.BUICK_LACROSSE,
|
||||
openpilotLongitudinalControl=True,
|
||||
networkLocation=CarParams.NetworkLocation.gateway,
|
||||
safetyConfigs=stock_safety,
|
||||
),
|
||||
True,
|
||||
)
|
||||
assert not supports_volt_auto_hold(
|
||||
SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT,
|
||||
|
||||
@@ -8,7 +8,13 @@ from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, DT_CTRL, structs
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.gm.carstate import CarState as GMCarState, get_hard_cruise_buttons, update_auto_hold_drive_timers
|
||||
from opendbc.car.gm.carstate import (
|
||||
CarState as GMCarState,
|
||||
get_hard_cruise_buttons,
|
||||
is_gm_auto_hold_active,
|
||||
update_auto_hold_drive_timers,
|
||||
update_startup_acc_fault_suppression,
|
||||
)
|
||||
from opendbc.car.gm.carcontroller import (
|
||||
VisualAlert,
|
||||
get_acc_dashboard_always_one,
|
||||
@@ -22,7 +28,7 @@ import opendbc.car.gm.interface as gm_interface
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.gps import CHEVROLET_BOLT_GPS_CARS, CHEVROLET_BOLT_GPS_MESSAGES, get_car_gps_config, parse_chevrolet_bolt_can_gps
|
||||
from opendbc.car.gm.fingerprints import FINGERPRINTS
|
||||
from opendbc.car.gm.values import ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, DBC, GM_RX_OFFSET, CarControllerParams, CruiseButtons, GMFlags, GMSafetyFlags
|
||||
from opendbc.car.gm.values import ALT_ACCS, ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, DBC, GM_RX_OFFSET, CarControllerParams, CruiseButtons, GMFlags, GMSafetyFlags
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
from openpilot.common.params import Params
|
||||
|
||||
@@ -204,7 +210,179 @@ class TestBoltGps:
|
||||
assert all(message in parsers[Bus.pt].vl for message in CHEVROLET_BOLT_GPS_MESSAGES)
|
||||
|
||||
|
||||
class TestGMCarState:
|
||||
@parameterized.expand([
|
||||
(CAR.BUICK_LACROSSE, True, True, True, True, False, True),
|
||||
(CAR.CHEVROLET_VOLT, True, True, True, True, False, True),
|
||||
(CAR.CHEVROLET_BOLT_CC_2017, True, True, True, True, False, False),
|
||||
(CAR.BUICK_LACROSSE, True, True, True, False, False, False),
|
||||
(CAR.BUICK_LACROSSE, True, True, True, True, True, False),
|
||||
])
|
||||
def test_auto_hold_alert_state_requires_supported_complete_stop(self, car_fingerprint, engaged, in_drive,
|
||||
cruise_available, standstill, gas_pressed, expected):
|
||||
assert is_gm_auto_hold_active(
|
||||
car_fingerprint, engaged, in_drive, cruise_available, standstill, gas_pressed,
|
||||
) is expected
|
||||
|
||||
def test_lacrosse_startup_acc_fault_is_suppressed(self):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
|
||||
)
|
||||
|
||||
assert suppressed
|
||||
assert timer == pytest.approx(5.0 - DT_CTRL)
|
||||
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 2, timer, 0, False,
|
||||
)
|
||||
|
||||
assert timer == 0.0
|
||||
assert not suppressed
|
||||
|
||||
def test_lacrosse_persistent_acc_fault_is_reported_after_startup(self):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
|
||||
)
|
||||
|
||||
for _ in range(int(5.0 / DT_CTRL)):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 2, timer, 3, False,
|
||||
)
|
||||
|
||||
assert timer == 0.0
|
||||
assert not suppressed
|
||||
|
||||
def test_lacrosse_brake_unavailable_fault_is_never_suppressed(self):
|
||||
_, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, True,
|
||||
)
|
||||
|
||||
assert not suppressed
|
||||
|
||||
def test_startup_acc_fault_suppression_is_scoped_to_lacrosse(self):
|
||||
_, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_REGAL, 2, 0, 0.0, 3, False,
|
||||
)
|
||||
|
||||
assert not suppressed
|
||||
|
||||
|
||||
class TestGMInterface:
|
||||
def test_suburban_obd_and_ascm_integrations_remain_separate(self):
|
||||
fingerprint = _empty_fingerprint()
|
||||
fingerprint[0] = FINGERPRINTS[CAR.CHEVROLET_SUBURBAN][0].copy()
|
||||
|
||||
assert CAR.CHEVROLET_SUBURBAN_ASCM in ASCM_INT
|
||||
assert FINGERPRINTS[CAR.CHEVROLET_SUBURBAN_ASCM] == FINGERPRINTS[CAR.CHEVROLET_SUBURBAN]
|
||||
|
||||
obd_params = interfaces[CAR.CHEVROLET_SUBURBAN].get_params(
|
||||
CAR.CHEVROLET_SUBURBAN,
|
||||
fingerprint,
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
ascm_params = interfaces[CAR.CHEVROLET_SUBURBAN_ASCM].get_params(
|
||||
CAR.CHEVROLET_SUBURBAN_ASCM,
|
||||
fingerprint,
|
||||
[],
|
||||
alpha_long=True,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
|
||||
assert obd_params.networkLocation == structs.CarParams.NetworkLocation.gateway
|
||||
assert obd_params.openpilotLongitudinalControl
|
||||
assert not obd_params.pcmCruise
|
||||
assert obd_params.safetyConfigs[0].safetyParam == 0
|
||||
|
||||
assert ascm_params.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
|
||||
assert not ascm_params.flags & GMFlags.SASCM.value
|
||||
assert not ascm_params.alphaLongitudinalAvailable
|
||||
assert not ascm_params.openpilotLongitudinalControl
|
||||
assert ascm_params.pcmCruise
|
||||
assert ascm_params.safetyConfigs[0].safetyParam == GMSafetyFlags.HW_CAM.value | GMSafetyFlags.HW_ASCM_INT.value
|
||||
assert ascm_params.lateralTuning.torque.latAccelFactor == pytest.approx(obd_params.lateralTuning.torque.latAccelFactor)
|
||||
assert ascm_params.lateralTuning.torque.friction == pytest.approx(obd_params.lateralTuning.torque.friction)
|
||||
|
||||
def test_suburban_camera_harness_preserves_stock_acc(self):
|
||||
fingerprint = _empty_fingerprint()
|
||||
fingerprint[0] = FINGERPRINTS[CAR.CHEVROLET_SUBURBAN][0].copy()
|
||||
fingerprint[2] = fingerprint[0].copy()
|
||||
|
||||
assert CAR.CHEVROLET_SUBURBAN_CAMERA in CAMERA_ACC_CAR
|
||||
assert CAR.CHEVROLET_SUBURBAN_CAMERA in ALT_ACCS
|
||||
assert CAR.CHEVROLET_SUBURBAN_CAMERA not in CC_ONLY_CAR
|
||||
assert CAR.CHEVROLET_SUBURBAN_CAMERA not in ASCM_INT
|
||||
assert all(fp[CAMERA_DIAGNOSTIC_ADDRESS] == 8 for fp in FINGERPRINTS[CAR.CHEVROLET_SUBURBAN_CAMERA])
|
||||
assert all(fp[CAMERA_DIAGNOSTIC_ADDRESS + GM_RX_OFFSET] == 8 for fp in FINGERPRINTS[CAR.CHEVROLET_SUBURBAN_CAMERA])
|
||||
|
||||
camera_params = interfaces[CAR.CHEVROLET_SUBURBAN_CAMERA].get_params(
|
||||
CAR.CHEVROLET_SUBURBAN_CAMERA,
|
||||
fingerprint,
|
||||
[],
|
||||
alpha_long=True,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
|
||||
assert camera_params.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
|
||||
assert camera_params.pcmCruise
|
||||
assert not camera_params.alphaLongitudinalAvailable
|
||||
assert not camera_params.openpilotLongitudinalControl
|
||||
assert camera_params.safetyConfigs[0].safetyParam == GMSafetyFlags.HW_CAM.value
|
||||
|
||||
def test_suburban_cc_remains_no_acc_gateway_profile(self):
|
||||
fingerprint = _empty_fingerprint()
|
||||
fingerprint[0] = FINGERPRINTS[CAR.CHEVROLET_SUBURBAN_CC][0].copy()
|
||||
|
||||
cc_params = interfaces[CAR.CHEVROLET_SUBURBAN_CC].get_params(
|
||||
CAR.CHEVROLET_SUBURBAN_CC,
|
||||
fingerprint,
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
|
||||
assert cc_params.networkLocation == structs.CarParams.NetworkLocation.gateway
|
||||
assert cc_params.openpilotLongitudinalControl
|
||||
assert not cc_params.pcmCruise
|
||||
assert cc_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_CC_LONG.value
|
||||
assert cc_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_NO_ACC.value
|
||||
|
||||
def test_lacrosse_obd_and_ascm_integrations_remain_separate(self):
|
||||
obd_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
||||
CAR.BUICK_LACROSSE,
|
||||
_empty_fingerprint(),
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
ascm_params = interfaces[CAR.BUICK_LACROSSE_ASCM].get_params(
|
||||
CAR.BUICK_LACROSSE_ASCM,
|
||||
_empty_fingerprint(),
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
|
||||
assert obd_params.networkLocation == structs.CarParams.NetworkLocation.gateway
|
||||
assert obd_params.openpilotLongitudinalControl
|
||||
assert obd_params.radarTimeStepDEPRECATED == pytest.approx(0.15)
|
||||
assert ascm_params.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
|
||||
assert not ascm_params.openpilotLongitudinalControl
|
||||
assert ascm_params.radarTimeStepDEPRECATED == pytest.approx(0.0667)
|
||||
|
||||
@parameterized.expand([
|
||||
CAR.CHEVROLET_BOLT_CC_2017,
|
||||
CAR.CHEVROLET_BOLT_CC_2018_2021,
|
||||
@@ -291,6 +469,14 @@ class TestGMInterface:
|
||||
|
||||
assert car_params.minSteerSpeed == pytest.approx(7 * CV.MPH_TO_MS)
|
||||
|
||||
def test_lacrosse_2019_ascm_min_steer_speed_is_28_mph(self):
|
||||
car_model = CAR.BUICK_LACROSSE_ASCM_19US
|
||||
CarInterface = interfaces[car_model]
|
||||
car_params = CarInterface.get_params(car_model, _empty_fingerprint(), [], alpha_long=False, is_release=False, docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles())
|
||||
|
||||
assert car_params.minSteerSpeed == pytest.approx(28 * CV.MPH_TO_MS)
|
||||
|
||||
@parameterized.expand([
|
||||
("interceptor", True),
|
||||
("ascm_int", False),
|
||||
@@ -486,6 +672,43 @@ class TestGMInterface:
|
||||
assert car_params.openpilotLongitudinalControl
|
||||
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
|
||||
|
||||
def test_buick_lacrosse_auto_hold_sets_stock_hold_safety_bit_with_op_long_enabled(self):
|
||||
params = Params()
|
||||
try:
|
||||
params.put_bool("GMAutoHold", True)
|
||||
car_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
||||
CAR.BUICK_LACROSSE,
|
||||
_empty_fingerprint(),
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
finally:
|
||||
params.remove("GMAutoHold")
|
||||
|
||||
assert car_params.openpilotLongitudinalControl
|
||||
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
|
||||
|
||||
def test_buick_lacrosse_auto_hold_is_off_when_toggle_is_disabled(self):
|
||||
params = Params()
|
||||
try:
|
||||
params.put_bool("GMAutoHold", False)
|
||||
car_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
||||
CAR.BUICK_LACROSSE,
|
||||
_empty_fingerprint(),
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
finally:
|
||||
params.remove("GMAutoHold")
|
||||
|
||||
assert not car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
|
||||
|
||||
def test_volt_auto_hold_does_not_set_stock_hold_safety_bit_with_op_long_disabled(self):
|
||||
CarInterface = interfaces[CAR.CHEVROLET_VOLT_ASCM]
|
||||
fingerprint = _empty_fingerprint()
|
||||
@@ -784,7 +1007,7 @@ class TestGMCarController:
|
||||
|
||||
def test_volt_cc_redneck_rate_limits_setpoint_changes_by_planner_acceleration(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(0.5 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
controller = SimpleNamespace(frame=int(0.2 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
@@ -800,17 +1023,224 @@ class TestGMCarController:
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
|
||||
packer, controller, cs, SimpleNamespace(accel=1.0), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
|
||||
controller.frame = int(0.7 / DT_CTRL)
|
||||
controller.frame = int(0.3 / DT_CTRL)
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=1.0), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
|
||||
def test_volt_cc_redneck_does_not_raise_stock_setpoint_above_max(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(2.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=100.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=100.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
assert controller.apply_speed == 100
|
||||
|
||||
def test_volt_cc_redneck_tracks_max_inside_request_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(3.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=52.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=52.0 * CV.KPH_TO_MS),
|
||||
vCruise=60.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.1), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 53
|
||||
|
||||
def test_volt_cc_redneck_tracks_max_down_inside_request_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(3.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=68.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=68.0 * CV.KPH_TO_MS),
|
||||
vCruise=60.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=-0.1), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 67
|
||||
|
||||
def test_volt_cc_redneck_holds_small_decel_request_at_max(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(3.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=100.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=100.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=-0.1), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
assert controller.apply_speed == 100
|
||||
|
||||
def test_volt_cc_redneck_holds_strong_decel_request_at_max_during_free_cruise(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(3.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=100.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=100.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=-1.2), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
assert controller.apply_speed == 100
|
||||
|
||||
def test_volt_cc_redneck_brakes_for_active_lead_inside_free_road_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(3.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=100.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=100.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=-0.5), SimpleNamespace(is_metric=True), longitudinal_adjustment_active=True,
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 99
|
||||
|
||||
def test_volt_cc_redneck_accelerates_when_pseudo_speed_request_exceeds_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(0.1 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=44.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=44.0 * CV.KPH_TO_MS),
|
||||
vCruise=50.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=2.0), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
|
||||
controller.frame = int(0.3 / DT_CTRL)
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=2.0), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 45
|
||||
|
||||
def test_volt_cc_redneck_brakes_when_pseudo_speed_request_exceeds_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=50.7 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=49.0 * CV.KPH_TO_MS),
|
||||
vCruise=49.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=-1.36), SimpleNamespace(is_metric=True), longitudinal_adjustment_active=True,
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 48
|
||||
|
||||
def test_volt_cc_no_camera_redneck_spam_stays_on_powertrain_bus(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
|
||||
@@ -357,6 +357,14 @@ class CAR(Platforms):
|
||||
[GMCarDocs("Chevrolet Suburban Premier 2016-20")],
|
||||
CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
|
||||
)
|
||||
CHEVROLET_SUBURBAN_ASCM = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Suburban Premier ASCM Harness 2016-20", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
CHEVROLET_SUBURBAN.specs,
|
||||
)
|
||||
CHEVROLET_SUBURBAN_CAMERA = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Suburban Premier Camera Harness 2016-20", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
CHEVROLET_SUBURBAN.specs,
|
||||
)
|
||||
GMC_YUKON_CC = GMPlatformConfig(
|
||||
[GMCarDocs("GMC Yukon No-ACC 2019-20")],
|
||||
CarSpecs(mass=2541, wheelbase=2.95, steerRatio=16.3, centerToFrontRatio=0.4),
|
||||
@@ -533,12 +541,21 @@ EV_CAR = {
|
||||
CAR.CHEVROLET_MALIBU_HYBRID_CC,
|
||||
}
|
||||
|
||||
GM_AUTO_HOLD_CARS = {
|
||||
CAR.CHEVROLET_VOLT,
|
||||
CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
CAR.BUICK_LACROSSE,
|
||||
}
|
||||
|
||||
# We're integrated at the camera with VOACC on these cars (instead of ASCM w/ OBD-II harness)
|
||||
CAMERA_ACC_CAR = {
|
||||
CAR.CHEVROLET_BOLT_ACC_2022_2023,
|
||||
CAR.CHEVROLET_SILVERADO,
|
||||
CAR.CHEVROLET_EQUINOX,
|
||||
CAR.CHEVROLET_TRAILBLAZER,
|
||||
CAR.CHEVROLET_SUBURBAN_CAMERA,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
CAR.CHEVROLET_BLAZER,
|
||||
CAR.CHEVROLET_TRAX,
|
||||
@@ -546,7 +563,7 @@ CAMERA_ACC_CAR = {
|
||||
}
|
||||
|
||||
# Alt ASCMActiveCruiseControlStatus
|
||||
ALT_ACCS = {CAR.GMC_YUKON, CAR.GMC_YUKON_CC}
|
||||
ALT_ACCS = {CAR.CHEVROLET_SUBURBAN_CAMERA, CAR.GMC_YUKON, CAR.GMC_YUKON_CC}
|
||||
|
||||
# We're integrated at the Safety Data Gateway Module on these cars
|
||||
SDGM_CAR = {
|
||||
@@ -585,9 +602,10 @@ CC_REGEN_PADDLE_CAR = {
|
||||
}
|
||||
CAMERA_ACC_CAR.update(CC_ONLY_CAR)
|
||||
|
||||
# ASCM-INT paths are only enabled when SASCM (0x2FF) is detected at runtime
|
||||
# ASCM-intercept variants preserve stock ACC. SASCM (0x2FF) enables alpha-long where supported.
|
||||
ASCM_INT = {
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_SUBURBAN_ASCM,
|
||||
CAR.GMC_ACADIA_ASCM,
|
||||
CAR.CHEVROLET_MALIBU_ASCM,
|
||||
CAR.CADILLAC_ESCALADE_ASCM,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,6 +23,20 @@ from openpilot.common.params import Params
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
LongCtrlState = structs.CarControl.Actuators.LongControlState
|
||||
|
||||
BOSCH_BRAKE_FORCE_ON = -0.12
|
||||
BOSCH_BRAKE_FORCE_RELEASE = -0.02
|
||||
|
||||
|
||||
def update_honda_bosch_braking(braking: bool, gas_pedal_force: float, stopping: bool, long_active: bool) -> bool:
|
||||
"""Select Bosch brake mode from the same road-load-adjusted force used for gas."""
|
||||
if not long_active:
|
||||
return False
|
||||
if stopping:
|
||||
return True
|
||||
if braking:
|
||||
return gas_pedal_force <= BOSCH_BRAKE_FORCE_RELEASE
|
||||
return gas_pedal_force < BOSCH_BRAKE_FORCE_ON
|
||||
|
||||
|
||||
def get_civic_bosch_modified_torque_lpf_tau(torque_cmd: float, prev_torque_cmd: float, v_ego: float) -> float:
|
||||
torque_delta = abs(float(torque_cmd) - float(prev_torque_cmd))
|
||||
@@ -238,6 +252,7 @@ class CarController(CarControllerBase):
|
||||
self.steering_pressed_filter_s = 0.0
|
||||
self.steering_pressed_robust_prev = False
|
||||
self.bosch_last_gas = 0.0
|
||||
self.bosch_braking = False
|
||||
self.bosch_gas_factor = self.param_store.get_float("HondaGasFactorParams", default=1.0)
|
||||
self.bosch_wind_factor = self.param_store.get_float("HondaWindFactorParams", default=1.0)
|
||||
self.bosch_wind_factor_before_brake = self.bosch_wind_factor
|
||||
@@ -472,12 +487,16 @@ class CarController(CarControllerBase):
|
||||
self.bosch_last_gas = self.gas
|
||||
|
||||
stopping = actuators.longControlState == LongCtrlState.stopping
|
||||
bosch_braking = None
|
||||
if not self.mvl_accord_mode:
|
||||
self.bosch_braking = update_honda_bosch_braking(self.bosch_braking, gas_pedal_force, stopping, CC.longActive)
|
||||
bosch_braking = self.bosch_braking
|
||||
self.stopping_counter = self.stopping_counter + 1 if stopping else 0
|
||||
if not self.mvl_accord_mode or mvl_radar_owned:
|
||||
can_sends.extend(
|
||||
hondacan.create_acc_commands(
|
||||
self.packer, self.CAN, CC.enabled, CC.longActive, self.accel, self.gas, self.stopping_counter, self.CP,
|
||||
gas_force=gas_pedal_force if self.mvl_accord_mode else None,
|
||||
gas_force=gas_pedal_force, braking=bosch_braking,
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -71,16 +71,18 @@ def create_brake_command(packer, CAN, apply_brake, pump_on, pcm_override, pcm_ca
|
||||
return packer.make_can_msg("BRAKE_COMMAND", CAN.pt, values)
|
||||
|
||||
|
||||
def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_counter, CP, gas_force=None):
|
||||
def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_counter, CP, gas_force=None, braking=None):
|
||||
commands = []
|
||||
min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0]
|
||||
|
||||
control_on = 5 if enabled else 0
|
||||
if gas_force is None:
|
||||
gas_force = accel
|
||||
gas_command = gas if active and gas_force > min_gas_accel else -30000
|
||||
if braking is None:
|
||||
braking = gas_force < min_gas_accel
|
||||
braking = int(active and braking)
|
||||
gas_command = gas if active and gas_force > min_gas_accel and not braking else -30000
|
||||
accel_command = accel if active else 0
|
||||
braking = 1 if active and gas_force < min_gas_accel else 0
|
||||
standstill = 1 if active and stopping_counter > 0 else 0
|
||||
standstill_release = 1 if active and stopping_counter == 0 else 0
|
||||
|
||||
|
||||
@@ -7,13 +7,16 @@ from opendbc.car.structs import CarParams
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.carcontroller import (
|
||||
BOSCH_BRAKE_FORCE_ON,
|
||||
BOSCH_BRAKE_FORCE_RELEASE,
|
||||
CarController,
|
||||
get_civic_bosch_modified_steering_pressed,
|
||||
get_civic_bosch_modified_torque_lpf_tau,
|
||||
get_honda_bosch_wind_brake_mps2,
|
||||
update_honda_bosch_braking,
|
||||
update_honda_bosch_live_learning,
|
||||
)
|
||||
from opendbc.car.honda.hondacan import create_lkas_hud
|
||||
from opendbc.car.honda.hondacan import create_acc_commands, create_lkas_hud
|
||||
from opendbc.car.honda.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.honda.values import CAR, DBC, HONDA_BOSCH, HONDA_BOSCH_TJA_CONTROL, CarControllerParams, HondaFlags, HondaSafetyFlags, \
|
||||
HondaStarPilotFlags
|
||||
@@ -26,6 +29,67 @@ def get_test_toggles() -> SimpleNamespace:
|
||||
|
||||
|
||||
class TestHondaFingerprint:
|
||||
@staticmethod
|
||||
def _acc_control_values(active, accel, gas=500, gas_force=0.5, braking=False):
|
||||
class FakePacker:
|
||||
@staticmethod
|
||||
def make_can_msg(name, bus, values):
|
||||
return name, bus, values
|
||||
|
||||
can = SimpleNamespace(pt=1)
|
||||
cp = SimpleNamespace(carFingerprint=CAR.HONDA_CRV_5G)
|
||||
commands = create_acc_commands(FakePacker(), can, True, active, accel, gas, 0, cp, gas_force, braking)
|
||||
assert commands[-1][0] == "ACC_CONTROL"
|
||||
return commands[-1][2]
|
||||
|
||||
def test_bosch_acc_commands_reject_fault_route_gas_brake_conflict(self):
|
||||
braking = update_honda_bosch_braking(False, 0.2, False, True)
|
||||
values = self._acc_control_values(True, -0.27, gas=160, gas_force=0.2, braking=braking)
|
||||
|
||||
assert values["GAS_COMMAND"] == 160
|
||||
assert values["ACCEL_COMMAND"] == pytest.approx(-0.27)
|
||||
assert values["BRAKE_REQUEST"] == 0
|
||||
assert values["BRAKE_LIGHTS"] == 0
|
||||
|
||||
@pytest.mark.parametrize("active", [False, True])
|
||||
@pytest.mark.parametrize("accel", [-3.5, -0.27, -0.2, -0.1, 0.0, 0.01, 2.0])
|
||||
@pytest.mark.parametrize("gas_force", [-0.5, 0.0, 0.5])
|
||||
@pytest.mark.parametrize("braking", [False, True])
|
||||
def test_bosch_acc_commands_never_request_gas_and_braking_together(self, active, accel, gas_force, braking):
|
||||
values = self._acc_control_values(active, accel, gas_force=gas_force, braking=braking)
|
||||
|
||||
assert not (values["GAS_COMMAND"] > 0 and values["BRAKE_REQUEST"] == 1)
|
||||
assert not (values["GAS_COMMAND"] > 0 and values["BRAKE_LIGHTS"] == 1)
|
||||
if values["GAS_COMMAND"] > 0:
|
||||
assert active
|
||||
|
||||
def test_bosch_acc_commands_preserve_road_load_gas_above_brake_threshold(self):
|
||||
values = self._acc_control_values(True, -0.27, gas=500, gas_force=0.3)
|
||||
|
||||
assert values["GAS_COMMAND"] == 500
|
||||
assert values["ACCEL_COMMAND"] == pytest.approx(-0.27)
|
||||
assert values["BRAKE_REQUEST"] == 0
|
||||
assert values["BRAKE_LIGHTS"] == 0
|
||||
|
||||
def test_bosch_acc_commands_do_not_send_gas_without_positive_force(self):
|
||||
values = self._acc_control_values(True, 0.2, gas=500, gas_force=-0.4)
|
||||
|
||||
assert values["GAS_COMMAND"] == -30000
|
||||
|
||||
def test_bosch_braking_uses_force_hysteresis(self):
|
||||
braking = update_honda_bosch_braking(False, BOSCH_BRAKE_FORCE_ON - 0.01, False, True)
|
||||
assert braking
|
||||
|
||||
braking = update_honda_bosch_braking(braking, -0.05, False, True)
|
||||
assert braking
|
||||
|
||||
braking = update_honda_bosch_braking(braking, BOSCH_BRAKE_FORCE_RELEASE + 0.01, False, True)
|
||||
assert not braking
|
||||
|
||||
def test_bosch_braking_preserves_stopping_and_resets_inactive(self):
|
||||
assert update_honda_bosch_braking(False, 0.5, True, True)
|
||||
assert not update_honda_bosch_braking(True, -1.0, False, False)
|
||||
|
||||
def test_honda_lkas_hud_shows_lane_lines_when_lateral_only_is_active(self):
|
||||
class FakePacker:
|
||||
@staticmethod
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,18 +4,20 @@ from dataclasses import dataclass
|
||||
# hkg-angle-steering-2025 branch at cc4b08625. See CREDITS.md and THIRD_PARTY_NOTICES.md.
|
||||
import numpy as np
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, rate_limit, structs
|
||||
from opendbc.car import Bus, DT_CTRL, create_gas_interceptor_command, make_tester_present_msg, rate_limit, structs
|
||||
from opendbc.car.common.filter_simple import FirstOrderFilter
|
||||
from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm, common_fault_avoidance, get_max_angle_delta_vm, get_max_angle_vm
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.hyundai import hyundaicanfd, hyundaican
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.lead_data import CanLeadDataState
|
||||
from opendbc.car.hyundai.values import HyundaiFlags, HyundaiSafetyFlags, HyundaiStarPilotFlags, Buttons, CarControllerParams, CAR, CANFD_ANGLE_LONGITUDINAL_CAR, \
|
||||
CANFD_RADAR_LIVE_LONGITUDINAL_CAR, CANFD_ALT_BUTTONS_RESUME_CAR, kia_ev6_gt_line_longitudinal_tuning, \
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR, CANFD_ALT_BUTTONS_RESUME_CAR, kia_ev6_gt_line_longitudinal_tuning, \
|
||||
KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import get_hyundai_canfd_scc_jerk_limits, shape_hyundai_canfd_scc_accel
|
||||
from openpilot.starpilot.common.testing_grounds import testing_ground
|
||||
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
@@ -40,6 +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.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]
|
||||
@@ -436,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)
|
||||
@@ -473,6 +474,7 @@ class CarController(CarControllerBase):
|
||||
self._ioniq_6_lane_change_ui_frames = 0
|
||||
self._ioniq_6_long_tuning = Ioniq6LongitudinalTuningState()
|
||||
self._genesis_g90_long_tuning = GenesisG90LongitudinalTuningState()
|
||||
self._can_lead_data = CanLeadDataState()
|
||||
self._dash_lat_disengage_blink_frame = 0
|
||||
self._dash_lat_disengage_init = False
|
||||
self._dash_prev_lat_active = False
|
||||
@@ -482,6 +484,9 @@ class CarController(CarControllerBase):
|
||||
CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CAN_REFRESH_MSGS
|
||||
)
|
||||
self._ray_lfa_packer = CANPacker("hyundai_kia_ray_lfa") if self._ray_lfa_8byte else None
|
||||
self._ray_pedal = CP.carFingerprint == CAR.KIA_RAY_EV and CP.enableGasInterceptorDEPRECATED
|
||||
self._ray_pedal_packer = CANPacker("hyundai_kia_ray_pedal") if self._ray_pedal else None
|
||||
self._ray_pedal_gas_last = 0.0
|
||||
|
||||
def _update_dash_icon_state(self, CC):
|
||||
if CC.latActive:
|
||||
@@ -502,7 +507,9 @@ class CarController(CarControllerBase):
|
||||
return lka_icon, lfa_icon
|
||||
|
||||
def _get_canfd_scc_lead_state(self, CC, CS, now_nanos):
|
||||
openpilot_lead_visible = bool(getattr(CS, "openpilot_lead_visible", False) or CC.hudControl.leadVisible)
|
||||
openpilot_lead_visible = bool(
|
||||
getattr(CS, "openpilot_lead_visible", False) or getattr(CC.hudControl, "leadVisible", False)
|
||||
)
|
||||
openpilot_lead_distance = float(np.clip(getattr(CS, "openpilot_lead_distance", 0.0), 0.0, 204.7))
|
||||
openpilot_lead_rel_speed = float(np.clip(getattr(CS, "openpilot_lead_rel_speed", 0.0), -16.4, 34.7))
|
||||
stock_camera_lead_fresh = now_nanos - getattr(CS, "stock_camera_lead_ts", 0) <= CANFD_CAMERA_LEAD_STALE_NS
|
||||
@@ -631,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
|
||||
@@ -756,14 +761,23 @@ class CarController(CarControllerBase):
|
||||
can_canfd_blended = bool(self.CP.flags & HyundaiFlags.CAN_CANFD_BLENDED)
|
||||
blended_hda2 = can_canfd_blended and bool(self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING)
|
||||
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
|
||||
lead_visible = bool(getattr(CS, "openpilot_lead_visible", False) or getattr(hud_control, "leadVisible", False))
|
||||
lead_distance = float(np.clip(getattr(CS, "openpilot_lead_distance", 0.0), 0.0, 204.7))
|
||||
lead_rel_speed = float(np.clip(getattr(CS, "openpilot_lead_rel_speed", 0.0), -170.0, 239.5))
|
||||
if lead_visible and lead_distance <= CANFD_LEAD_MIN_DISTANCE:
|
||||
lead_distance = CANFD_FALLBACK_LEAD_DISTANCE
|
||||
lead_rel_speed = 0.0
|
||||
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:
|
||||
can_sends.extend(hyundaicanfd.create_steering_messages(
|
||||
self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_torque, 0.0,
|
||||
lka_icon=lka_icon,
|
||||
longitudinal_active=longitudinal_active,
|
||||
))
|
||||
if self.long_active_ecu:
|
||||
@@ -774,6 +788,7 @@ class CarController(CarControllerBase):
|
||||
left_lane_warning, right_lane_warning, CS.msg_364,
|
||||
include_alerts=False,
|
||||
counter_mod=0xF,
|
||||
fcw_opt_usm=2 if apply_steer_req or lka_icon == 3 else 1,
|
||||
))
|
||||
if self.frame % 5 == 0:
|
||||
can_sends.append(hyundaicanfd.create_suppress_lfa(
|
||||
@@ -797,9 +812,13 @@ class CarController(CarControllerBase):
|
||||
|
||||
# Button messages
|
||||
if not self.long_active_ecu:
|
||||
if self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
|
||||
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:
|
||||
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:
|
||||
# send 25 messages at a time to increases the likelihood of resume being accepted
|
||||
@@ -807,7 +826,39 @@ class CarController(CarControllerBase):
|
||||
if (self.frame - self.last_button_frame) * DT_CTRL >= 0.15:
|
||||
self.last_button_frame = self.frame
|
||||
else:
|
||||
can_sends.extend(self._create_can_redneck_button_messages(CS))
|
||||
if not self._ray_pedal:
|
||||
can_sends.extend(self._create_can_redneck_button_messages(CS))
|
||||
|
||||
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)
|
||||
if pedal_active:
|
||||
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(
|
||||
self._ray_pedal_packer, self._ray_pedal_gas_last, (self.frame // 4) & 0xF))
|
||||
|
||||
if self.long_active_ecu and can_canfd_blended:
|
||||
if blended_hda2:
|
||||
@@ -835,7 +886,7 @@ class CarController(CarControllerBase):
|
||||
can_sends.extend(hyundaican.create_acc_commands(self.packer, CC.enabled, accel, jerk, int(self.frame / 2),
|
||||
hud_control, set_speed_in_units, stopping,
|
||||
CC.cruiseControl.override, use_fca, self.CP,
|
||||
main_cruise_enabled))
|
||||
main_cruise_enabled, lead_data))
|
||||
|
||||
# 20 Hz LFA MFA message
|
||||
if self.frame % 5 == 0 and (self.CP.flags & HyundaiFlags.SEND_LFA.value or (self.long_active_ecu and blended_hda2)):
|
||||
@@ -859,8 +910,13 @@ class CarController(CarControllerBase):
|
||||
can_sends = []
|
||||
|
||||
lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING
|
||||
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
|
||||
lfa_longitudinal_active = longitudinal_active if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else self.CP.openpilotLongitudinalControl
|
||||
persistent_lfa_status_cars = (
|
||||
CAR.HYUNDAI_IONIQ_6,
|
||||
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
|
||||
CAR.KIA_EV6,
|
||||
)
|
||||
lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \
|
||||
if self.CP.carFingerprint in persistent_lfa_status_cars else self.long_active_ecu
|
||||
lka_steering_long = lka_steering and lfa_longitudinal_active
|
||||
ccnc_non_hda2 = self.CP.flags & HyundaiFlags.CCNC and not lka_steering
|
||||
use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \
|
||||
@@ -887,7 +943,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
gear = getattr(getattr(CS, "out", None), "gearShifter", None)
|
||||
drive_gear = gear == structs.CarState.GearShifter.drive
|
||||
if angle_lkas_alt:
|
||||
if angle_lkas_alt and self.CP.carFingerprint != CAR.KIA_SPORTAGE_HEV_2026:
|
||||
steering_msg_active = bool(steering_msg_active and drive_gear)
|
||||
angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive)
|
||||
forward_stock_lkas = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and angle_lkas_alt and (
|
||||
@@ -917,7 +973,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
# prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU
|
||||
suppress_lfa = bool(lka_steering)
|
||||
if angle_lkas_alt:
|
||||
if angle_lkas_alt and self.CP.carFingerprint != CAR.KIA_SPORTAGE_HEV_2026:
|
||||
suppress_lfa = bool(lka_steering and drive_gear and (CC.latActive or (ccnc_angle_long and CC.enabled)))
|
||||
if self.frame % 5 == 0 and suppress_lfa:
|
||||
can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.lfa_block_msg,
|
||||
@@ -987,12 +1043,17 @@ class CarController(CarControllerBase):
|
||||
left_sound_active=left_warning.sound_active, right_sound_active=right_warning.sound_active,
|
||||
)
|
||||
else:
|
||||
adrv_messages = hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame)
|
||||
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,
|
||||
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.
|
||||
radar_heartbeat_step = 1 if ccnc_angle_long else 4
|
||||
if self.CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR and self.frame % radar_heartbeat_step == 0:
|
||||
if self.CP.carFingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR and self.frame % radar_heartbeat_step == 0:
|
||||
can_sends.append(hyundaicanfd.create_accelerator_brake_alt_spoof(0, self.frame // radar_heartbeat_step,
|
||||
CS.out.brakePressed, CS.out.gasPressed,
|
||||
self.CP.carFingerprint))
|
||||
@@ -1016,10 +1077,23 @@ class CarController(CarControllerBase):
|
||||
CC.leftBlinker,
|
||||
CC.rightBlinker))
|
||||
if self.frame % 2 == 0:
|
||||
lead_visible, lead_distance, lead_rel_speed = self._get_canfd_scc_lead_state(CC, CS, now_nanos)
|
||||
if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
||||
acc_kwargs = {}
|
||||
scc_jerk_limits = get_hyundai_canfd_scc_jerk_limits(self.CP, stopping, accel)
|
||||
raw_accel = accel
|
||||
accel = shape_hyundai_canfd_scc_accel(
|
||||
self.CP, CC.enabled, CC.cruiseControl.override, stopping, accel, self.accel_last,
|
||||
)
|
||||
acc_kwargs = {
|
||||
"direct_accel": True,
|
||||
"raw_accel": raw_accel,
|
||||
"jerk_upper": scc_jerk_limits[0],
|
||||
"jerk_lower": scc_jerk_limits[1],
|
||||
"lead_distance": lead_distance,
|
||||
"lead_rel_speed": lead_rel_speed,
|
||||
"lead_visible": lead_visible,
|
||||
}
|
||||
else:
|
||||
lead_visible, lead_distance, lead_rel_speed = self._get_canfd_scc_lead_state(CC, CS, now_nanos)
|
||||
acc_kwargs = {
|
||||
"main_mode_acc": int(CS.out.cruiseState.available),
|
||||
"direct_accel": True,
|
||||
|
||||
@@ -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,16 @@ 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
|
||||
|
||||
self.cruise_info = {}
|
||||
self.msg_161 = {}
|
||||
@@ -178,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
|
||||
@@ -300,6 +341,7 @@ class CarState(CarStateBase):
|
||||
cp = can_parsers[Bus.pt]
|
||||
cp_cam = can_parsers[Bus.cam]
|
||||
cp_alt = can_parsers.get(Bus.alt)
|
||||
cp_pedal = can_parsers.get(Bus.party)
|
||||
|
||||
if self.CP.flags & HyundaiFlags.CANFD:
|
||||
return self.update_canfd(can_parsers)
|
||||
@@ -393,6 +435,11 @@ class CarState(CarStateBase):
|
||||
ret.espDisabled = cp.vl["TCS11"]["TCS_PAS"] == 1
|
||||
ret.espActive = cp.vl["TCS11"]["ABS_ACT"] == 1
|
||||
ret.accFaulted = False if no_scc else cp.vl["TCS13"]["ACCEnable"] != 0 # 0 ACC CONTROL ENABLED, 1-3 ACC CONTROL DISABLED
|
||||
if self.CP.carFingerprint == CAR.KIA_RAY_EV and self.CP.enableGasInterceptorDEPRECATED:
|
||||
self.ray_pedal_valid = bool(cp_pedal is not None and cp_pedal.can_valid and
|
||||
cp_pedal.ts_nanos["GAS_SENSOR"]["STATE"] > 0)
|
||||
self.ray_pedal_state = int(cp_pedal.vl["GAS_SENSOR"]["STATE"]) if cp_pedal is not None else 5
|
||||
ret.accFaulted = not self.ray_pedal_valid or self.ray_pedal_state != 0
|
||||
|
||||
if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV | HyundaiFlags.FCEV):
|
||||
if self.CP.flags & HyundaiFlags.FCEV:
|
||||
@@ -404,6 +451,12 @@ class CarState(CarStateBase):
|
||||
else:
|
||||
ret.gasPressed = bool(cp.vl["EMS16"]["CF_Ems_AclAct"])
|
||||
|
||||
if self.CP.carFingerprint == CAR.KIA_RAY_EV and self.CP.enableGasInterceptorDEPRECATED and self.ray_pedal_valid:
|
||||
driver_pedal = cp_pedal.vl_raw["GAS_SENSOR"]
|
||||
track1 = int.from_bytes(driver_pedal[:2], "big")
|
||||
track2 = int.from_bytes(driver_pedal[2:4], "big")
|
||||
ret.gasPressed = track1 > 272 or track2 > 513
|
||||
|
||||
# Gear Selection via Cluster - For those Kia/Hyundai which are not fully discovered, we can use the Cluster Indicator for Gear Selection,
|
||||
# as this seems to be standard over all cars, but is not the preferred method.
|
||||
if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV):
|
||||
@@ -625,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)
|
||||
|
||||
@@ -748,4 +808,6 @@ class CarState(CarStateBase):
|
||||
}
|
||||
if CP.carFingerprint in ALT_BUS_LDA_BUTTON_CARS:
|
||||
parsers[Bus.alt] = CANParser(DBC[CP.carFingerprint][Bus.pt], [("CLU13", 0)], 1)
|
||||
if CP.carFingerprint == CAR.KIA_RAY_EV and CP.enableGasInterceptorDEPRECATED:
|
||||
parsers[Bus.party] = CANParser("hyundai_kia_ray_pedal", [("GAS_SENSOR", 50)], 0)
|
||||
return parsers
|
||||
|
||||
@@ -185,6 +185,7 @@ FW_VERSIONS = {
|
||||
],
|
||||
(Ecu.abs, 0x7d1, None): [
|
||||
b'\xf1\x00DN ESC \x01 102\x19\x04\x13 58910-L1300',
|
||||
b'\xf1\x00DN ESC \x01 107 \x07\x03 58910-L1300',
|
||||
b'\xf1\x00DN ESC \x03 100 \x08\x01 58910-L0300',
|
||||
b'\xf1\x00DN ESC \x06 104\x19\x08\x01 58910-L0100',
|
||||
b'\xf1\x00DN ESC \x06 106 \x07\x01 58910-L0100',
|
||||
@@ -208,6 +209,7 @@ FW_VERSIONS = {
|
||||
b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0210\x00 4DNAC102',
|
||||
b'\xf1\x00DN8 MDPS C 1.00 1.03 56310-L1010 4DNDC103',
|
||||
b'\xf1\x00DN8 MDPS C 1.00 1.03 56310-L1030 4DNDC103',
|
||||
b'\xf1\x00DN8 MDPS C 1.00 1.03 56310-L1210 4DNDC103',
|
||||
b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP100',
|
||||
b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP101',
|
||||
b'\xf1\x00DN8 MDPS R 1.00 1.02 57700-L1000 4DNDP105',
|
||||
@@ -215,6 +217,7 @@ FW_VERSIONS = {
|
||||
(Ecu.fwdCamera, 0x7c4, None): [
|
||||
b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.02 99211-L1000 190422',
|
||||
b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.04 99211-L1000 191016',
|
||||
b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.06 99211-L1000 210325',
|
||||
b'\xf1\x00DN8 MFC AT RUS LHD 1.00 1.03 99211-L1000 190705',
|
||||
b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.00 99211-L0000 190716',
|
||||
b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.01 99211-L0000 191016',
|
||||
@@ -1332,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 ',
|
||||
@@ -1553,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): [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crcmod
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.lead_data import CanLeadData
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
|
||||
hyundai_checksum = crcmod.mkCrcFun(0x11D, initCrc=0xFD, rev=False, xorOut=0xdf)
|
||||
@@ -51,7 +52,7 @@ def create_lkas11(packer, frame, CP, apply_torque, steer_req,
|
||||
# FcwOpt_USM 2 = Green car + lanes
|
||||
# FcwOpt_USM 1 = White car + lanes
|
||||
# FcwOpt_USM 0 = No car + lanes
|
||||
values["CF_Lkas_FcwOpt_USM"] = lka_icon if CP.carFingerprint == CAR.GENESIS_G70_2020 else 2 if enabled else 1
|
||||
values["CF_Lkas_FcwOpt_USM"] = lka_icon
|
||||
|
||||
# SysWarning 4 = keep hands on wheel
|
||||
# SysWarning 5 = keep hands on wheel (red)
|
||||
@@ -128,13 +129,13 @@ def create_lkas11_can_canfd_blended(packer, frame, CP, apply_steer, steer_req,
|
||||
torque_fault, lkas11, sys_warning, sys_state, enabled,
|
||||
left_lane, right_lane,
|
||||
left_lane_depart, right_lane_depart, msg_364,
|
||||
include_alerts=True, counter_mod=0x10):
|
||||
include_alerts=True, counter_mod=0x10, fcw_opt_usm=None):
|
||||
bus = CanBus(CP).ECAN
|
||||
values = {
|
||||
"CF_Lkas_LdwsActivemode": int(left_lane) + (int(right_lane) << 1),
|
||||
"CF_Lkas_LdwsLHWarning": left_lane_depart,
|
||||
"CF_Lkas_LdwsRHWarning": right_lane_depart,
|
||||
"CF_Lkas_FcwOpt_USM": 2 if enabled else 1,
|
||||
"CF_Lkas_FcwOpt_USM": (2 if enabled else 1) if fcw_opt_usm is None else fcw_opt_usm,
|
||||
"CR_Lkas_StrToqReq": apply_steer,
|
||||
"CF_Lkas_ActToi": steer_req,
|
||||
"CF_Lkas_ToiFlt": torque_fault,
|
||||
@@ -317,19 +318,20 @@ def create_acc_commands_can_canfd_blended_hda2(packer, enabled, accel, accel_las
|
||||
|
||||
|
||||
def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, set_speed, stopping, long_override, use_fca, CP,
|
||||
main_cruise_enabled=True):
|
||||
main_cruise_enabled=True, lead_data: CanLeadData | None = None):
|
||||
commands = []
|
||||
lead_data = lead_data or CanLeadData()
|
||||
|
||||
scc11_values = {
|
||||
"MainMode_ACC": int(bool(main_cruise_enabled)),
|
||||
"TauGapSet": hud_control.leadDistanceBars,
|
||||
"VSetDis": set_speed if enabled else 0,
|
||||
"AliveCounterACC": idx % 0x10,
|
||||
"ObjValid": 1, # close lead makes controls tighter
|
||||
"ACC_ObjStatus": 1, # close lead makes controls tighter
|
||||
"ObjValid": int(lead_data.lead_visible),
|
||||
"ACC_ObjStatus": int(lead_data.lead_visible),
|
||||
"ACC_ObjLatPos": 0,
|
||||
"ACC_ObjRelSpd": 0,
|
||||
"ACC_ObjDist": 1, # close lead makes controls tighter
|
||||
"ACC_ObjRelSpd": lead_data.lead_rel_speed,
|
||||
"ACC_ObjDist": int(lead_data.lead_distance),
|
||||
}
|
||||
commands.append(packer.make_can_msg("SCC11", 0, scc11_values))
|
||||
|
||||
@@ -357,7 +359,8 @@ def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, se
|
||||
"JerkUpperLimit": upper_jerk, # stock usually is 1.0 but sometimes uses higher values
|
||||
"JerkLowerLimit": 5.0, # stock usually is 0.5 but sometimes uses higher values
|
||||
"ACCMode": 2 if enabled and long_override else 1 if enabled else 4, # stock will always be 4 instead of 0 after first disengage
|
||||
"ObjGap": 2 if hud_control.leadVisible else 0, # 5: >30, m, 4: 25-30 m, 3: 20-25 m, 2: < 20 m, 0: no lead
|
||||
"ObjGap": lead_data.object_gap, # 5: >30 m, 4: 25-30 m, 3: 20-25 m, 2: <20 m, 0: no lead
|
||||
"ObjDistStat": lead_data.object_rel_gap,
|
||||
}
|
||||
commands.append(packer.make_can_msg("SCC14", 0, scc14_values))
|
||||
|
||||
|
||||
@@ -8,6 +8,37 @@ from opendbc.car.crc import CRC16_XMODEM
|
||||
from opendbc.car.hyundai.values import HyundaiFlags, CAR, CANFD_ALT_BUTTONS_RESUME_CAR
|
||||
|
||||
|
||||
_adrv_0x51_templates: dict[CAR, bytes] = {}
|
||||
|
||||
|
||||
def cache_adrv_0x51_template(car_fingerprint: CAR, dat: bytes | None) -> None:
|
||||
if car_fingerprint not in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN):
|
||||
return
|
||||
|
||||
if dat is None:
|
||||
_adrv_0x51_templates.pop(car_fingerprint, None)
|
||||
elif len(dat) == 32 and any(dat[3:]):
|
||||
_adrv_0x51_templates[car_fingerprint] = bytes(dat)
|
||||
|
||||
|
||||
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, {})
|
||||
|
||||
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
|
||||
return CanData(0x51, bytes(dat), CAN.ACAN)
|
||||
|
||||
|
||||
def _set_value(msg: bytearray, sig, ival: int) -> None:
|
||||
i = sig.lsb // 8
|
||||
bits = sig.size
|
||||
@@ -123,7 +154,12 @@ def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_torque,
|
||||
else:
|
||||
lkas_values = copy.copy(control_values)
|
||||
lkas_values["LKA_AVAILABLE"] = 0
|
||||
if CP.carFingerprint in (CAR.KIA_CARNIVAL_4TH_GEN, CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN):
|
||||
if CP.carFingerprint in (
|
||||
CAR.KIA_CARNIVAL_4TH_GEN,
|
||||
CAR.KIA_CARNIVAL_2025,
|
||||
CAR.KIA_CARNIVAL_HEV_4TH_GEN,
|
||||
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
|
||||
):
|
||||
lkas_values["DAMP_FACTOR"] = 100
|
||||
|
||||
if lfa_base_values:
|
||||
@@ -142,7 +178,21 @@ def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_torque,
|
||||
lkas_values["LKAS_ANGLE_ACTIVE"] = 2 if lat_active else 1
|
||||
lkas_values["ADAS_ACIAnglTqRedcGainVal"] = apply_torque if lat_active else 0.0
|
||||
if angle_lkas_alt:
|
||||
if lat_active:
|
||||
if CP.carFingerprint == CAR.KIA_SPORTAGE_HEV_2026:
|
||||
lkas_values = {
|
||||
"LKA_OptUsmSta": 0,
|
||||
"LKA_SysIndReq": 2 if enabled else 1,
|
||||
"StrTqReqVal": 0,
|
||||
"LKA_SysWrn": 0,
|
||||
"ActToiSta": 0,
|
||||
"LKA_UsmMod": 0,
|
||||
"LKA_RcgSta": 3 if lat_active else 0,
|
||||
"Damping_Gain": 100,
|
||||
"ADAS_StrAnglReqVal": apply_angle,
|
||||
"LKAS_ANGLE_ACTIVE": 2 if lat_active else 1,
|
||||
"ADAS_ACIAnglTqRedcGainVal": apply_torque if lat_active else 0.0,
|
||||
}
|
||||
elif lat_active:
|
||||
lkas_values = {
|
||||
"LKA_OptUsmSta": 0,
|
||||
"LKA_RcgSta": 3,
|
||||
@@ -685,13 +735,13 @@ def create_ioniq_6_cluster_lane_change_messages(CAN, frame, side=None):
|
||||
|
||||
def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control,
|
||||
main_mode_acc=1, jerk_lower=None, jerk_upper=None, direct_accel=False,
|
||||
lead_distance=None, lead_rel_speed=None, lead_visible=None, cruise_info=None):
|
||||
lead_distance=None, lead_rel_speed=None, lead_visible=None, cruise_info=None, raw_accel=None):
|
||||
jerk = 5
|
||||
jn = jerk / 50
|
||||
if not enabled or gas_override:
|
||||
a_val, a_raw = 0, 0
|
||||
elif direct_accel:
|
||||
a_raw = accel
|
||||
a_raw = accel if raw_accel is None else raw_accel
|
||||
a_val = accel
|
||||
else:
|
||||
a_raw = accel
|
||||
@@ -769,15 +819,14 @@ def create_fca_warning_light(packer, CAN, frame):
|
||||
return ret
|
||||
|
||||
|
||||
def create_adrv_messages(packer, CAN, frame, blended_hda2=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 = []
|
||||
|
||||
values = {
|
||||
}
|
||||
ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values))
|
||||
ret.append(create_adrv_0x51(packer, CAN, frame, car_fingerprint, drive_gear, v_ego))
|
||||
|
||||
if blended_hda2:
|
||||
return ret
|
||||
|
||||
@@ -2,12 +2,13 @@ import time
|
||||
# Provenance: portions of HKG angle integration are adapted from sunnypilot/opendbc's
|
||||
# hkg-angle-steering-2025 branch at cc4b08625. See CREDITS.md and THIRD_PARTY_NOTICES.md.
|
||||
from opendbc.car import get_safety_config, structs, uds
|
||||
from opendbc.car.hyundai import hyundaicanfd
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.values import HyundaiFlags, CAR, CarControllerParams, \
|
||||
CANFD_UNSUPPORTED_LONGITUDINAL_CAR, \
|
||||
CANFD_SECURITYACCESS_CAR, \
|
||||
CANFD_ANGLE_LONGITUDINAL_CAR, \
|
||||
CANFD_RADAR_LIVE_LONGITUDINAL_CAR, \
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR, \
|
||||
RADAR_LIVE_LONGITUDINAL_CAR, \
|
||||
UNSUPPORTED_LONGITUDINAL_CAR, HyundaiSafetyFlags, \
|
||||
LEGACY_LONGITUDINAL_CAR, \
|
||||
@@ -27,6 +28,15 @@ from openpilot.starpilot.common.testing_grounds import testing_ground
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
Ecu = structs.CarParams.Ecu
|
||||
|
||||
|
||||
def get_communication_control_request(car_fingerprint):
|
||||
if car_fingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR:
|
||||
return bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX,
|
||||
uds.MESSAGE_TYPE.NORMAL])
|
||||
|
||||
return bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX,
|
||||
uds.MESSAGE_TYPE.NORMAL])
|
||||
|
||||
# Cancel button can sometimes be ACC pause/resume button, main button can also enable on some cars
|
||||
ENABLE_BUTTONS = (ButtonType.accelCruise, ButtonType.decelCruise, ButtonType.cancel, ButtonType.mainCruise)
|
||||
|
||||
@@ -34,6 +44,7 @@ ENABLE_BUTTONS = (ButtonType.accelCruise, ButtonType.decelCruise, ButtonType.can
|
||||
ECU_DISABLE_TIMESTAMP = 0.0
|
||||
KONA_NON_SCC_FCA_RADAR_ADDR = 0x602
|
||||
KIA_EV9_ACCEL_MAX = 2.2
|
||||
RAY_PEDAL_SENSOR_ADDR = 0x201
|
||||
|
||||
|
||||
def apply_platform_longitudinal_params(ret: structs.CarParams) -> None:
|
||||
@@ -190,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
|
||||
@@ -293,6 +306,18 @@ class CarInterface(CarInterfaceBase):
|
||||
elif ret.flags & HyundaiFlags.FCEV:
|
||||
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.FCEV_GAS.value
|
||||
|
||||
if (candidate == CAR.KIA_RAY_EV and fingerprint[0].get(RAY_PEDAL_SENSOR_ADDR) == 6 and
|
||||
ret.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CAN_REFRESH_MSGS and
|
||||
ret.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.HAS_LDA_BUTTON):
|
||||
ret.enableGasInterceptorDEPRECATED = True
|
||||
ret.alphaLongitudinalAvailable = True
|
||||
ret.openpilotLongitudinalControl = True
|
||||
ret.pcmCruise = False
|
||||
ret.radarUnavailable = True
|
||||
ret.autoResumeSng = False
|
||||
ret.minEnableSpeed = -1.0
|
||||
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value
|
||||
|
||||
# Car specific configuration overrides
|
||||
|
||||
if candidate == CAR.GENESIS_G90:
|
||||
@@ -308,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
|
||||
@@ -353,14 +378,7 @@ class CarInterface(CarInterfaceBase):
|
||||
params = Params()
|
||||
|
||||
if communication_control is None:
|
||||
if CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR:
|
||||
# Don't use 0x80 suppress bit so we can read the ECU response.
|
||||
# Use ENABLE_RX_DISABLE_TX (0x01) so the ECU can still receive from rear radars for BSM
|
||||
# while blocking SCC TX.
|
||||
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
|
||||
else:
|
||||
# 0x80 silences response for other cars (original behavior)
|
||||
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
|
||||
communication_control = get_communication_control_request(CP.carFingerprint)
|
||||
|
||||
ecu_log(f"=== init() called: opLong={CP.openpilotLongitudinalControl}, flags=0x{CP.flags:x}, safetyParam={CP.safetyConfigs[-1].safetyParam} ===")
|
||||
|
||||
@@ -376,11 +394,25 @@ class CarInterface(CarInterfaceBase):
|
||||
skip_disable_ecu = True
|
||||
|
||||
if not skip_disable_ecu:
|
||||
disable_can_recv = can_recv
|
||||
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
|
||||
|
||||
def disable_can_recv(*args, **kwargs):
|
||||
packets = base_can_recv(*args, **kwargs)
|
||||
for packet in packets or []:
|
||||
for msg in packet:
|
||||
if msg.src == adrv_bus and msg.address == 0x51:
|
||||
hyundaicanfd.cache_adrv_0x51_template(CP.carFingerprint, msg.dat)
|
||||
return packets
|
||||
|
||||
# Try ECU disable. If it succeeds (IGN-ON mode), enable longitudinal.
|
||||
# If it fails (READY mode returns NRC 0x22, or timeout), strip LONG safety flag
|
||||
# so panda forwards stock SCC messages normally (lateral-only mode).
|
||||
ecu_log(f"=== ECU DISABLE attempt: addr=0x{addr:x}, bus={bus} ===")
|
||||
ecu_disabled = disable_ecu(can_recv, can_send, bus=bus, addr=addr, com_cont_req=communication_control,
|
||||
ecu_disabled = disable_ecu(disable_can_recv, can_send, bus=bus, addr=addr, com_cont_req=communication_control,
|
||||
reset=bool(CP.flags & HyundaiFlags.CAN_CANFD_BLENDED))
|
||||
|
||||
if CP.carFingerprint in (CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5_PE):
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanLeadData:
|
||||
object_gap: int = 0
|
||||
lead_distance: float = 0.0
|
||||
lead_rel_speed: float = 0.0
|
||||
lead_visible: bool = False
|
||||
|
||||
@property
|
||||
def object_rel_gap(self) -> int:
|
||||
return 0 if self.lead_distance == 0 else 2 if self.lead_rel_speed < -0.2 else 1
|
||||
|
||||
|
||||
def _hysteresis_update(current, new_value, counter, threshold):
|
||||
if new_value == current:
|
||||
return current, 0
|
||||
|
||||
counter += 1
|
||||
return (new_value, 0) if counter >= threshold else (current, counter)
|
||||
|
||||
|
||||
class CanLeadDataState:
|
||||
LEAD_HYSTERESIS_FRAMES = 50
|
||||
|
||||
def __init__(self):
|
||||
self._lead_on_counter = 0
|
||||
self._lead_off_counter = 0
|
||||
self._gap_counter = 0
|
||||
self._lead_visible = False
|
||||
self._object_gap = 0
|
||||
|
||||
@staticmethod
|
||||
def _get_object_gap(lead_distance: float) -> int:
|
||||
if lead_distance == 0:
|
||||
return 0
|
||||
if lead_distance < 20:
|
||||
return 2
|
||||
if lead_distance < 25:
|
||||
return 3
|
||||
if lead_distance < 30:
|
||||
return 4
|
||||
return 5
|
||||
|
||||
def update(self, lead_distance: float, lead_rel_speed: float, lead_visible: bool) -> CanLeadData:
|
||||
counter = self._lead_on_counter if lead_visible else self._lead_off_counter
|
||||
self._lead_visible, counter = _hysteresis_update(
|
||||
self._lead_visible, lead_visible, counter, self.LEAD_HYSTERESIS_FRAMES,
|
||||
)
|
||||
if lead_visible:
|
||||
self._lead_on_counter = counter
|
||||
self._lead_off_counter = 0
|
||||
else:
|
||||
self._lead_off_counter = counter
|
||||
self._lead_on_counter = 0
|
||||
|
||||
object_gap = self._get_object_gap(lead_distance)
|
||||
self._object_gap, self._gap_counter = _hysteresis_update(
|
||||
self._object_gap, object_gap, self._gap_counter, self.LEAD_HYSTERESIS_FRAMES,
|
||||
)
|
||||
|
||||
return CanLeadData(self._object_gap, lead_distance, lead_rel_speed, self._lead_visible)
|
||||
@@ -19,6 +19,9 @@ MRR30_RADAR_START_ADDR = 0x210
|
||||
MRR30_RADAR_MSG_COUNT = 16
|
||||
MRR35_RADAR_START_ADDR = 0x3A5
|
||||
MRR35_RADAR_MSG_COUNT = 32
|
||||
GV70_RADAR_START_ADDR = 0x210
|
||||
GV70_RADAR_MSG_COUNT = 16
|
||||
GV70_RADAR_DBC = "hyundai_radar_210_21f_generated"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -30,6 +33,7 @@ class RadarTrackConfig:
|
||||
frequency: int = 50
|
||||
parser_msg_count: int | None = None
|
||||
expected_length: int | None = None
|
||||
dbc_name: str | None = None
|
||||
|
||||
@property
|
||||
def can_parser_msg_count(self) -> int:
|
||||
@@ -47,6 +51,10 @@ RADAR_TRACK_CONFIGS = {
|
||||
|
||||
|
||||
def get_radar_track_config(car_fingerprint, flags: int = 0) -> RadarTrackConfig | None:
|
||||
if car_fingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
||||
return RadarTrackConfig(GV70_RADAR_START_ADDR, GV70_RADAR_MSG_COUNT, "gv70_210", bus=0,
|
||||
frequency=20, expected_length=32, dbc_name=GV70_RADAR_DBC)
|
||||
|
||||
radar_dbc = DBC[car_fingerprint].get(Bus.radar)
|
||||
if car_fingerprint == CAR.GENESIS_G90 and radar_dbc == HYUNDAI_MANDO_FRONT_RADAR_DBC:
|
||||
return RadarTrackConfig(RADAR_START_ADDR, G90_RADAR_MSG_COUNT, "mando", parser_msg_count=RADAR_MSG_COUNT)
|
||||
@@ -65,6 +73,10 @@ def radar_tracks_available(radar_config: RadarTrackConfig | None, fingerprint) -
|
||||
if radar_config is None:
|
||||
return False
|
||||
|
||||
if radar_config.radar_type == "gv70_210":
|
||||
return all(fingerprint[radar_config.bus].get(addr) == radar_config.expected_length
|
||||
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.msg_count))
|
||||
|
||||
msg_len = fingerprint[radar_config.bus].get(radar_config.start_addr)
|
||||
if msg_len is None:
|
||||
return False
|
||||
@@ -78,7 +90,8 @@ def get_radar_can_parser(CP, radar_config):
|
||||
|
||||
messages = [(f"RADAR_TRACK_{addr:x}", radar_config.frequency)
|
||||
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.can_parser_msg_count)]
|
||||
return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, radar_config.bus)
|
||||
dbc_name = radar_config.dbc_name or DBC[CP.carFingerprint][Bus.radar]
|
||||
return CANParser(dbc_name, messages, radar_config.bus)
|
||||
|
||||
|
||||
class RadarInterface(RadarInterfaceBase):
|
||||
@@ -223,6 +236,27 @@ class RadarInterface(RadarInterfaceBase):
|
||||
del self.pts[track_key]
|
||||
continue
|
||||
|
||||
if radar_type == "gv70_210":
|
||||
for i in ("1", "2"):
|
||||
track_key = addr * 2 + int(i) - 1
|
||||
valid = msg[f"{i}_STATE"] in (3, 4)
|
||||
if valid:
|
||||
pt = self.pts.get(track_key)
|
||||
if pt is None:
|
||||
pt = structs.RadarData.RadarPoint()
|
||||
pt.trackId = self.track_id
|
||||
self.track_id += 1
|
||||
self.pts[track_key] = pt
|
||||
pt.measured = True
|
||||
pt.dRel = msg[f"{i}_LONG_DIST"]
|
||||
pt.yRel = msg[f"{i}_LAT_DIST"]
|
||||
pt.vRel = msg[f"{i}_REL_SPEED"]
|
||||
pt.aRel = msg[f"{i}_REL_ACCEL"]
|
||||
pt.yvRel = msg[f"{i}_REL_LAT_SPEED"]
|
||||
elif track_key in self.pts:
|
||||
del self.pts[track_key]
|
||||
continue
|
||||
|
||||
if radar_type == "mrrevo14f":
|
||||
for i in ("1", "2"):
|
||||
track_key = addr * 2 + int(i) - 1
|
||||
|
||||
@@ -20,20 +20,21 @@ 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
|
||||
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX, get_communication_control_request
|
||||
from opendbc.car.hyundai import hyundaican, hyundaicanfd
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum
|
||||
from opendbc.car.hyundai.lead_data import CanLeadData, CanLeadDataState
|
||||
from opendbc.car.hyundai.radar_interface import MRREVO14F_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, MRR35_RADAR_START_ADDR, \
|
||||
RADAR_START_ADDR, get_radar_track_config
|
||||
RADAR_START_ADDR, RadarInterface, get_radar_track_config, radar_tracks_available
|
||||
from opendbc.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, DATELESS_FUZZY_CARS, \
|
||||
HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, CANFD_FUZZY_WHITELIST, \
|
||||
UNSUPPORTED_LONGITUDINAL_CAR, PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \
|
||||
LEGACY_LONGITUDINAL_CAR, DBC, HyundaiFlags, get_platform_codes, HyundaiSafetyFlags, \
|
||||
HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, Buttons, CarControllerParams, kia_ev6_gt_line_longitudinal_tuning
|
||||
HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, Buttons, CarControllerParams, \
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR, CANFD_RADAR_LIVE_LONGITUDINAL_CAR, kia_ev6_gt_line_longitudinal_tuning
|
||||
|
||||
LongCtrlState = CarControl.Actuators.LongControlState
|
||||
from opendbc.car.hyundai.fingerprints import FW_VERSIONS
|
||||
@@ -129,6 +130,145 @@ 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])
|
||||
|
||||
assert CAR.KIA_EV6 in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
|
||||
assert CAR.KIA_EV6 not in CANFD_RADAR_ECU_KEEPALIVE_CAR
|
||||
assert get_communication_control_request(CAR.KIA_EV6) == stock_request
|
||||
|
||||
assert CAR.HYUNDAI_IONIQ_5 in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
|
||||
assert CAR.HYUNDAI_IONIQ_5 not in CANFD_RADAR_ECU_KEEPALIVE_CAR
|
||||
assert get_communication_control_request(CAR.HYUNDAI_IONIQ_5) == stock_request
|
||||
|
||||
assert CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
|
||||
assert CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN not in CANFD_RADAR_ECU_KEEPALIVE_CAR
|
||||
assert get_communication_control_request(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) == stock_request
|
||||
|
||||
assert get_communication_control_request(CAR.HYUNDAI_IONIQ_6) == radar_keepalive_request
|
||||
|
||||
def test_ev6_adrv_0x51_replays_factory_payload(self):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.KIA_EV6
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.EV)
|
||||
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
|
||||
can_bus = CanBus(CP)
|
||||
factory = bytes.fromhex("88ed2e091700ffff5e0d0000012006ff021c2200000000000800000010000000")
|
||||
|
||||
hyundaicanfd.cache_adrv_0x51_template(CAR.KIA_EV6, factory)
|
||||
try:
|
||||
address, dat, bus = hyundaicanfd.create_adrv_0x51(packer, can_bus, 7, CAR.KIA_EV6, drive_gear=True)
|
||||
_, parked_dat, _ = hyundaicanfd.create_adrv_0x51(packer, can_bus, 8, CAR.KIA_EV6, drive_gear=False)
|
||||
_, other_dat, _ = hyundaicanfd.create_adrv_0x51(packer, can_bus, 7, CAR.HYUNDAI_IONIQ_6)
|
||||
finally:
|
||||
hyundaicanfd.cache_adrv_0x51_template(CAR.KIA_EV6, None)
|
||||
|
||||
assert address == 0x51
|
||||
assert bus == can_bus.ACAN
|
||||
assert dat[2] == (factory[2] + 8) & 0xFF
|
||||
assert dat[3:] == factory[3:]
|
||||
assert int.from_bytes(dat[:2], "little") == hkg_can_fd_checksum(address, None, bytearray(dat))
|
||||
assert parked_dat[3] == factory[3] & ~0x1
|
||||
assert parked_dat[4:] == factory[4:]
|
||||
assert int.from_bytes(parked_dat[:2], "little") == hkg_can_fd_checksum(address, None, bytearray(parked_dat))
|
||||
assert other_dat[3:] == bytes(29)
|
||||
|
||||
def test_ev6_init_captures_factory_adrv_0x51(self, monkeypatch):
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[CanBus(None, fingerprint).CAM][0x50] = 16
|
||||
radar_config = get_radar_track_config(CAR.KIA_EV6)
|
||||
fingerprint[radar_config.bus][radar_config.start_addr] = radar_config.expected_length
|
||||
car_fw = [CarParams.CarFw(ecu=Ecu.adas, fwVersion=b"", address=0x730, brand="hyundai")]
|
||||
CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, car_fw, True, False, False, get_test_toggles())
|
||||
factory = bytes.fromhex("6b657d090900e1ff000000000020ffff00000000000000000800000010000000")
|
||||
|
||||
def can_recv(*, wait_for_one=True):
|
||||
msg = SimpleNamespace(address=0x51, src=CanBus(CP).ACAN, dat=factory)
|
||||
return [[msg]]
|
||||
|
||||
def fake_disable_ecu(capturing_can_recv, *_args, **_kwargs):
|
||||
capturing_can_recv(wait_for_one=True)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("opendbc.car.hyundai.interface.disable_ecu", fake_disable_ecu)
|
||||
CarInterface.init(CP, can_recv, None)
|
||||
|
||||
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
|
||||
try:
|
||||
_, dat, _ = hyundaicanfd.create_adrv_0x51(packer, CanBus(CP), 0, CAR.KIA_EV6, drive_gear=True)
|
||||
finally:
|
||||
hyundaicanfd.cache_adrv_0x51_template(CAR.KIA_EV6, None)
|
||||
|
||||
assert dat[3:] == factory[3:]
|
||||
|
||||
def test_carnival_hev_low_speed_torque_rate_limits(self):
|
||||
CP = CarInterface.get_params(CAR.KIA_CARNIVAL_HEV_4TH_GEN, gen_empty_fingerprint(), [],
|
||||
False, False, False, None)
|
||||
@@ -426,6 +566,42 @@ class TestHyundaiFingerprint:
|
||||
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANFD_LKA_STEERING
|
||||
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.EV_GAS
|
||||
|
||||
gv70_radar_config = get_radar_track_config(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
|
||||
assert gv70_radar_config.radar_type == "gv70_210"
|
||||
for addr in range(gv70_radar_config.start_addr, gv70_radar_config.start_addr + gv70_radar_config.msg_count):
|
||||
gv70_fingerprint[gv70_radar_config.bus][addr] = gv70_radar_config.expected_length
|
||||
assert radar_tracks_available(gv70_radar_config, gv70_fingerprint)
|
||||
|
||||
CP = CarInterface.get_params(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, gv70_fingerprint, gv70_car_fw,
|
||||
True, False, False, None)
|
||||
assert not CP.radarUnavailable
|
||||
|
||||
radar = RadarInterface(CP)
|
||||
packer = CANPacker(gv70_radar_config.dbc_name)
|
||||
messages = []
|
||||
for addr in range(gv70_radar_config.start_addr, gv70_radar_config.start_addr + gv70_radar_config.msg_count):
|
||||
message = packer.make_can_msg(f"RADAR_TRACK_{addr:x}", 0, {
|
||||
"1_STATE": 3,
|
||||
"1_LONG_DIST": 25.0,
|
||||
"1_LAT_DIST": 0.5,
|
||||
"1_REL_SPEED": -2.0,
|
||||
"1_REL_LAT_SPEED": 0.1,
|
||||
"1_REL_ACCEL": -0.2,
|
||||
})
|
||||
data = bytearray(message[1])
|
||||
checksum = hkg_can_fd_checksum(addr, None, data)
|
||||
data[0] = checksum & 0xff
|
||||
data[1] = (checksum >> 8) & 0xff
|
||||
messages.append((message[0], bytes(data), message[2]))
|
||||
radar_data = radar.update([(1, messages)])
|
||||
assert radar_data is not None
|
||||
assert len(radar_data.points) == 16
|
||||
assert radar_data.points[0].dRel == pytest.approx(25.0)
|
||||
|
||||
other_config = get_radar_track_config(CAR.HYUNDAI_IONIQ_5)
|
||||
assert other_config.radar_type == "mrr30"
|
||||
assert other_config.dbc_name is None
|
||||
|
||||
for candidate in HYUNDAI_NON_SCC_CARS:
|
||||
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, None)
|
||||
assert bool(CP.flags & HyundaiFlags.NON_SCC)
|
||||
@@ -443,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
|
||||
|
||||
@@ -570,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"
|
||||
@@ -671,6 +847,45 @@ class TestHyundaiFingerprint:
|
||||
} <= msg_addrs_buses
|
||||
assert (0x364, 1) not in msg_addrs_buses
|
||||
|
||||
def test_palisade_telluride_hda2_aol_keeps_lkas_status_after_long_cancel(self):
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[2][0x50] = 16
|
||||
car_fw = [CarParams.CarFw(ecu=Ecu.adas, fwVersion=b"", address=0x730, brand="hyundai")]
|
||||
CP = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, car_fw, True, False, False, None)
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
controller.frame = 1
|
||||
|
||||
hud_control = SimpleNamespace(
|
||||
visualAlert=CarControl.HUDControl.VisualAlert.none,
|
||||
leftLaneVisible=True,
|
||||
rightLaneVisible=True,
|
||||
leftLaneDepart=False,
|
||||
rightLaneDepart=False,
|
||||
leadDistanceBars=3,
|
||||
leadVisible=False,
|
||||
)
|
||||
lfa_block_msg = {f"BYTE{i}": 0 for i in range(3, 24) if i != 7}
|
||||
lfa_block_msg["COUNTER"] = 0
|
||||
CS = SimpleNamespace(lfa_block_msg=lfa_block_msg, redneck_send_button=Buttons.NONE, lkas11={}, msg_364={},
|
||||
out=SimpleNamespace(vEgoRaw=5.0))
|
||||
CC = SimpleNamespace(enabled=False, latActive=True, longActive=False,
|
||||
cruiseControl=SimpleNamespace(cancel=False, resume=False, override=False))
|
||||
actuators = SimpleNamespace(longControlState=LongCtrlState.off)
|
||||
adas_parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS", 0)], 0)
|
||||
ecan_parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0)], 1)
|
||||
|
||||
for steering_requested, icon, expected_status in ((True, 2, 2), (False, 1, 1)):
|
||||
msgs = controller.create_can_msgs(steering_requested, 16 if steering_requested else 0, False,
|
||||
0.0, 0.0, False, hud_control, actuators, CS, CC, icon, icon)
|
||||
adas_parser.update([(1, [msg for msg in msgs if msg[0] == 0x50])])
|
||||
ecan_parser.update([(1, [msg for msg in msgs if msg[0] == 0x340])])
|
||||
|
||||
assert adas_parser.vl["LKAS"]["LKA_ICON"] == icon
|
||||
assert adas_parser.vl["LKAS"]["STEER_REQ"] == int(steering_requested)
|
||||
assert ecan_parser.vl["LKAS11"]["CF_Lkas_FcwOpt_USM"] == expected_status
|
||||
assert ecan_parser.vl["LKAS11"]["CF_Lkas_ActToi"] == int(steering_requested)
|
||||
assert not any(msg[0] == 0x364 for msg in msgs)
|
||||
|
||||
def test_g70_aol_uses_active_lkas_icon(self):
|
||||
CP = CarInterface.get_params(CAR.GENESIS_G70_2020, gen_empty_fingerprint(), [], False, False, False, None)
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
@@ -693,6 +908,48 @@ class TestHyundaiFingerprint:
|
||||
|
||||
assert parser.vl["LKAS11"]["CF_Lkas_FcwOpt_USM"] == 2
|
||||
|
||||
@pytest.mark.parametrize(("candidate", "expected_status"), (
|
||||
(CAR.KIA_NIRO_PHEV_2022, 2),
|
||||
(CAR.KIA_NIRO_HEV_2021, 2),
|
||||
))
|
||||
def test_classic_niro_aol_keeps_active_lkas_status(self, candidate, expected_status):
|
||||
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, get_test_toggles())
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
controller.frame = 1
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0)], 0)
|
||||
|
||||
hud_control = SimpleNamespace(
|
||||
visualAlert=CarControl.HUDControl.VisualAlert.none,
|
||||
leftLaneVisible=True,
|
||||
rightLaneVisible=True,
|
||||
leftLaneDepart=False,
|
||||
rightLaneDepart=False,
|
||||
leadVisible=False,
|
||||
)
|
||||
CS = SimpleNamespace(lkas11=parser.vl["LKAS11"])
|
||||
CC = SimpleNamespace(
|
||||
enabled=False,
|
||||
latActive=True,
|
||||
longActive=False,
|
||||
cruiseControl=SimpleNamespace(cancel=False, resume=False, override=False),
|
||||
)
|
||||
actuators = SimpleNamespace(longControlState=LongCtrlState.off)
|
||||
|
||||
msgs = controller.create_can_msgs(True, 156, False, 0.0, 0.0, False, hud_control, actuators, CS, CC, 2, 0)
|
||||
lkas11 = next(msg for msg in msgs if msg[0] == 0x340)
|
||||
parser.update([(1, [lkas11])])
|
||||
|
||||
assert parser.vl["LKAS11"]["CF_Lkas_ActToi"] == 1
|
||||
assert parser.vl["LKAS11"]["CF_Lkas_FcwOpt_USM"] == expected_status
|
||||
|
||||
CC.latActive = False
|
||||
msgs = controller.create_can_msgs(False, 0, False, 0.0, 0.0, False, hud_control, actuators, CS, CC, 1, 0)
|
||||
lkas11 = next(msg for msg in msgs if msg[0] == 0x340)
|
||||
parser.update([(2, [lkas11])])
|
||||
|
||||
assert parser.vl["LKAS11"]["CF_Lkas_ActToi"] == 0
|
||||
assert parser.vl["LKAS11"]["CF_Lkas_FcwOpt_USM"] == 1
|
||||
|
||||
def test_kona_non_scc_uses_no_individual_lane_lkas_status(self):
|
||||
CP = CarInterface.get_params(CAR.HYUNDAI_KONA_NON_SCC, gen_empty_fingerprint(), [], False, False, False, None)
|
||||
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
|
||||
@@ -896,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
|
||||
@@ -1101,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):
|
||||
@@ -1440,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()
|
||||
@@ -1484,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
|
||||
|
||||
@@ -2480,14 +2922,15 @@ class TestHyundaiFingerprint:
|
||||
assert parser.vl["LKAS_ALT"]["ADAS_ACIAnglTqRedcGainVal"] == pytest.approx(0.0)
|
||||
assert parser.vl["LKAS_ALT"]["ADAS_StrAnglReqVal"] == pytest.approx(8.5)
|
||||
|
||||
def test_gv70_electrified_uses_generic_lkas_status_payload(self):
|
||||
def test_gv70_electrified_uses_clean_damped_lkas_status_payload(self):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
|
||||
CP.openpilotLongitudinalControl = False
|
||||
CP.openpilotLongitudinalControl = True
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
controller.frame = 1
|
||||
controller.long_active_ecu = True
|
||||
can_bus = CanBus(CP)
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS", 0)], can_bus.ACAN)
|
||||
stock_lkas = {
|
||||
@@ -2508,6 +2951,7 @@ class TestHyundaiFingerprint:
|
||||
"DAMP_FACTOR": 100,
|
||||
}
|
||||
cc = SimpleNamespace(enabled=True, latActive=True,
|
||||
longActive=False,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False,
|
||||
hudControl=SimpleNamespace())
|
||||
@@ -2523,13 +2967,12 @@ class TestHyundaiFingerprint:
|
||||
parser.update([(1, lkas_msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["LKAS"]["HAS_LANE_SAFETY"] == 0
|
||||
assert parser.vl["LKAS"]["DAMP_FACTOR"] == 0
|
||||
assert parser.vl["LKAS"]["DAMP_FACTOR"] == 100
|
||||
assert parser.vl["LKAS"]["TORQUE_REQUEST"] == 0
|
||||
assert parser.vl["LKAS"]["STEER_REQ"] == 1
|
||||
assert parser.vl["LKAS"]["STEER_MODE"] == 0
|
||||
assert parser.vl["LKAS"]["NEW_SIGNAL_2"] == 0
|
||||
assert parser.vl["LKAS"]["STEER_MODE"] == 2
|
||||
assert parser.vl["LKAS"]["NEW_SIGNAL_2"] == 3
|
||||
|
||||
CP.openpilotLongitudinalControl = True
|
||||
lfa_parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LFA", 0)], can_bus.ECAN)
|
||||
lfa_msgs = hyundaicanfd.create_steering_messages(controller.packer, CP, can_bus, True, True, 0, 0.0)
|
||||
assert [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in lfa_msgs] == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
||||
@@ -2537,13 +2980,12 @@ class TestHyundaiFingerprint:
|
||||
assert lfa_parser.can_valid
|
||||
assert lfa_parser.vl["LFA"]["DAMP_FACTOR"] == 100
|
||||
|
||||
controller.long_active_ecu = True
|
||||
cc.longActive = False
|
||||
inactive_msgs = controller.create_canfd_msgs(0, True, 0.44, 0.0, 0.0, 0.0, False,
|
||||
cc.hudControl, cs, cc, get_test_toggles(), lka_icon=2, lfa_icon=2)
|
||||
steering_names = [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in inactive_msgs
|
||||
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
|
||||
assert steering_names == [("LKAS", can_bus.ACAN)]
|
||||
assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
||||
|
||||
controller.frame = 1
|
||||
cc.longActive = True
|
||||
@@ -2553,9 +2995,67 @@ class TestHyundaiFingerprint:
|
||||
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
|
||||
assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
||||
|
||||
def test_ioniq_6_keeps_lfa_status_when_longitudinal_is_inactive(self):
|
||||
def test_gv70_electrified_stock_long_uses_damped_lkas_request(self):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.HYUNDAI_IONIQ_6
|
||||
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
|
||||
CP.openpilotLongitudinalControl = False
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
can_bus = CanBus(CP)
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS", 0)], can_bus.ACAN)
|
||||
msgs = hyundaicanfd.create_steering_messages(controller.packer, CP, can_bus, True, True, 123, 0.0)
|
||||
|
||||
assert [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in msgs] == [("LKAS", can_bus.ACAN)]
|
||||
parser.update([(1, msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["LKAS"]["TORQUE_REQUEST"] == 123
|
||||
assert parser.vl["LKAS"]["STEER_REQ"] == 1
|
||||
assert parser.vl["LKAS"]["HAS_LANE_SAFETY"] == 0
|
||||
assert parser.vl["LKAS"]["DAMP_FACTOR"] == 100
|
||||
assert parser.vl["LKAS"]["STEER_MODE"] == 2
|
||||
assert parser.vl["LKAS"]["NEW_SIGNAL_2"] == 3
|
||||
|
||||
@pytest.mark.parametrize(("car", "powertrain_flag"), [
|
||||
(CAR.HYUNDAI_IONIQ_5, HyundaiFlags.EV),
|
||||
(CAR.HYUNDAI_IONIQ_6, HyundaiFlags.EV),
|
||||
(CAR.KIA_EV6, HyundaiFlags.EV),
|
||||
(CAR.KIA_CARNIVAL_2025, 0),
|
||||
(CAR.KIA_CARNIVAL_HEV_4TH_GEN, HyundaiFlags.HYBRID),
|
||||
(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, HyundaiFlags.EV),
|
||||
])
|
||||
def test_hda2_keeps_lfa_status_when_longitudinal_is_inactive(self, car, powertrain_flag):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = car
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_LKA_STEERING | powertrain_flag)
|
||||
CP.openpilotLongitudinalControl = True
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
cc = SimpleNamespace(
|
||||
enabled=False, latActive=False, longActive=False,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace(),
|
||||
)
|
||||
cs = SimpleNamespace(
|
||||
stock_lfa_msg=None, stock_lkas_msg=None,
|
||||
left_blindspot_from_radar=False, right_blindspot_from_radar=False,
|
||||
out=SimpleNamespace(gearShifter=structs.CarState.GearShifter.park),
|
||||
)
|
||||
|
||||
controller.frame = 1
|
||||
controller.long_active_ecu = True
|
||||
msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False,
|
||||
cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1)
|
||||
assert any(addr == 0x12A for addr, _, _ in msgs)
|
||||
|
||||
@pytest.mark.parametrize("car", [
|
||||
CAR.HYUNDAI_IONIQ_6,
|
||||
CAR.KIA_EV6,
|
||||
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
|
||||
])
|
||||
def test_egmp_persistent_lfa_status_survives_ecu_fallback_state(self, car):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = car
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
|
||||
CP.openpilotLongitudinalControl = True
|
||||
|
||||
@@ -2569,6 +3069,7 @@ class TestHyundaiFingerprint:
|
||||
)
|
||||
cs = SimpleNamespace(
|
||||
stock_lfa_msg=None, stock_lkas_msg=None,
|
||||
left_blindspot_from_radar=False, right_blindspot_from_radar=False,
|
||||
out=SimpleNamespace(gearShifter=structs.CarState.GearShifter.park),
|
||||
)
|
||||
|
||||
@@ -2591,10 +3092,11 @@ class TestHyundaiFingerprint:
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.pid),
|
||||
cruiseControl=SimpleNamespace(override=False, cancel=False, resume=False),
|
||||
leftBlinker=False, rightBlinker=False,
|
||||
hudControl=SimpleNamespace(leadDistanceBars=3),
|
||||
hudControl=SimpleNamespace(leadDistanceBars=3, leadVisible=True),
|
||||
)
|
||||
cs = SimpleNamespace(
|
||||
stock_lfa_msg=None, stock_lkas_msg=None,
|
||||
openpilot_lead_visible=True, openpilot_lead_distance=37.5, openpilot_lead_rel_speed=-1.3,
|
||||
out=SimpleNamespace(steeringAngleDeg=0.0, gearShifter=structs.CarState.GearShifter.drive),
|
||||
)
|
||||
|
||||
@@ -2607,7 +3109,8 @@ class TestHyundaiFingerprint:
|
||||
parser.update([(1, scc_msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["SCC_CONTROL"]["MainMode_ACC"] == 1
|
||||
assert parser.vl["SCC_CONTROL"]["ACC_ObjDist"] == pytest.approx(1.0)
|
||||
assert parser.vl["SCC_CONTROL"]["ACC_ObjDist"] == pytest.approx(37.5)
|
||||
assert parser.vl["SCC_CONTROL"]["ACC_ObjRelSpd"] == pytest.approx(-1.3)
|
||||
assert parser.vl["SCC_CONTROL"]["ObjValid"] == 0
|
||||
assert parser.vl["SCC_CONTROL"]["aReqValue"] == pytest.approx(-0.1)
|
||||
assert parser.vl["SCC_CONTROL"]["aReqRaw"] == pytest.approx(-1.0)
|
||||
@@ -2708,7 +3211,7 @@ class TestHyundaiFingerprint:
|
||||
assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs
|
||||
|
||||
@pytest.mark.parametrize("standstill", [False, True])
|
||||
def test_sportage_angle_lkas_alt_keeps_inactive_status_in_drive(self, standstill):
|
||||
def test_sportage_angle_lkas_alt_keeps_status_and_suppression_alive(self, standstill):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
|
||||
@@ -2716,45 +3219,14 @@ class TestHyundaiFingerprint:
|
||||
CP.openpilotLongitudinalControl = False
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
controller.frame = 5
|
||||
can_bus = CanBus(CP)
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
|
||||
stock_lkas = {
|
||||
"CHECKSUM": 1234,
|
||||
"COUNTER": 42,
|
||||
"LKA_OptUsmSta": 2,
|
||||
"LKA_MODE": 2,
|
||||
"LKA_RcgSta": 3,
|
||||
"LKA_AVAILABLE": 3,
|
||||
"LKA_LHLnWrnSta": 3,
|
||||
"LKA_RHLnWrnSta": 3,
|
||||
"LKA_WARNING": 1,
|
||||
"LKA_HndsoffSnd": 1,
|
||||
"LKA_StrSnd": 1,
|
||||
"LKA_SysIndReq": 4,
|
||||
"LKA_ICON": 2,
|
||||
"FCA_SYSWARN": 1,
|
||||
"StrTqReqVal": 17,
|
||||
"TORQUE_REQUEST": 17,
|
||||
"ActToiSta": 3,
|
||||
"STEER_REQ": 1,
|
||||
"ToiFltSta": 3,
|
||||
"LFA_BUTTON": 1,
|
||||
"LKA_SysWrn": 15,
|
||||
"LKA_ASSIST": 1,
|
||||
"Damping_Gain": 0,
|
||||
"STEER_MODE": 5,
|
||||
"NEW_SIGNAL_2": 0,
|
||||
"LKAS_ANGLE_ACTIVE": 2,
|
||||
"LKA_UsmMod": 3,
|
||||
"HAS_LANE_SAFETY": 1,
|
||||
"ADAS_StrAnglReqVal": 12.3,
|
||||
"ADAS_ACIAnglTqRedcGainVal": 0.42,
|
||||
"DAMP_FACTOR": 0,
|
||||
}
|
||||
cc = SimpleNamespace(enabled=False, latActive=False,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
|
||||
cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg=stock_lkas,
|
||||
lfa_block_msg = {f"BYTE{i}": 0 for i in range(3, 32) if i != 7}
|
||||
lfa_block_msg["COUNTER"] = 0
|
||||
cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg={}, lfa_block_msg=lfa_block_msg,
|
||||
out=SimpleNamespace(standstill=standstill, steeringAngleDeg=0.0,
|
||||
gearShifter=structs.CarState.GearShifter.drive))
|
||||
|
||||
@@ -2762,14 +3234,52 @@ class TestHyundaiFingerprint:
|
||||
get_test_toggles(), lka_icon=1, lfa_icon=1)
|
||||
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110]
|
||||
assert len(lkas_msgs) == 1
|
||||
assert len([msg for msg in msgs if msg[0] == 0x362]) == 1
|
||||
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
|
||||
parser.update([(1, lkas_msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["LKAS_ALT"]["LKA_StrSnd"] == 2
|
||||
assert parser.vl["LKAS_ALT"]["LKA_OptUsmSta"] == 0
|
||||
assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 1
|
||||
assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 0
|
||||
assert parser.vl["LKAS_ALT"]["LKA_AVAILABLE"] == 0
|
||||
assert parser.vl["LKAS_ALT"]["Damping_Gain"] == 100
|
||||
assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1
|
||||
assert parser.vl["LKAS_ALT"]["ADAS_ACIAnglTqRedcGainVal"] == 0.0
|
||||
|
||||
def test_sportage_angle_lkas_alt_active_status_matches_vehicle_contract(self):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
|
||||
HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.CANFD_LKA_STEERING_ALT)
|
||||
CP.openpilotLongitudinalControl = False
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
controller.frame = 5
|
||||
can_bus = CanBus(CP)
|
||||
cc = SimpleNamespace(enabled=True, latActive=True,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
|
||||
lfa_block_msg = {f"BYTE{i}": 0 for i in range(3, 32) if i != 7}
|
||||
lfa_block_msg["COUNTER"] = 0
|
||||
cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg={}, lfa_block_msg=lfa_block_msg,
|
||||
out=SimpleNamespace(standstill=False, steeringAngleDeg=10.0,
|
||||
gearShifter=structs.CarState.GearShifter.drive))
|
||||
|
||||
msgs = controller.create_canfd_msgs(0, True, 0.4, 12.0, 0.0, 0.0, False, cc.hudControl, cs, cc,
|
||||
get_test_toggles(), lka_icon=2, lfa_icon=2)
|
||||
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110]
|
||||
assert len(lkas_msgs) == 1
|
||||
assert len([msg for msg in msgs if msg[0] == 0x362]) == 1
|
||||
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
|
||||
parser.update([(1, lkas_msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["LKAS_ALT"]["LKA_OptUsmSta"] == 0
|
||||
assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 2
|
||||
assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 3
|
||||
assert parser.vl["LKAS_ALT"]["Damping_Gain"] == 100
|
||||
assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 2
|
||||
assert parser.vl["LKAS_ALT"]["ADAS_ACIAnglTqRedcGainVal"] == pytest.approx(0.4)
|
||||
|
||||
def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self):
|
||||
CP = CarParams.new_message()
|
||||
@@ -2895,6 +3405,50 @@ class TestHyundaiFingerprint:
|
||||
assert parser.vl["SCC14"]["ComfortBandUpper"] == pytest.approx(0.0)
|
||||
assert parser.vl["SCC14"]["ComfortBandLower"] == pytest.approx(0.0)
|
||||
assert parser.vl["SCC14"]["JerkLowerLimit"] == pytest.approx(5.0)
|
||||
assert parser.vl["SCC11"]["ObjValid"] == 0
|
||||
assert parser.vl["SCC11"]["ACC_ObjDist"] == 0
|
||||
assert parser.vl["SCC14"]["ObjGap"] == 0
|
||||
assert parser.vl["SCC14"]["ObjDistStat"] == 0
|
||||
|
||||
def test_can_acc_commands_show_approaching_lead(self):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.HYUNDAI_ELANTRA_2021
|
||||
|
||||
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("SCC11", 0), ("SCC14", 0)], 0)
|
||||
lead_data = CanLeadData(object_gap=4, lead_distance=27.5, lead_rel_speed=-1.3, lead_visible=True)
|
||||
|
||||
msgs = hyundaican.create_acc_commands(packer, enabled=True, accel=0.0, upper_jerk=1.0, idx=3,
|
||||
hud_control=SimpleNamespace(leadDistanceBars=3), set_speed=42,
|
||||
stopping=False, long_override=False, use_fca=False, CP=CP,
|
||||
lead_data=lead_data)
|
||||
parser.update([(1, msgs)])
|
||||
|
||||
assert parser.can_valid
|
||||
assert parser.vl["SCC11"]["ObjValid"] == 1
|
||||
assert parser.vl["SCC11"]["ACC_ObjStatus"] == 1
|
||||
assert parser.vl["SCC11"]["ACC_ObjDist"] == pytest.approx(27.0)
|
||||
assert parser.vl["SCC11"]["ACC_ObjRelSpd"] == pytest.approx(-1.3)
|
||||
assert parser.vl["SCC14"]["ObjGap"] == 4
|
||||
assert parser.vl["SCC14"]["ObjDistStat"] == 2
|
||||
|
||||
def test_can_lead_data_hysteresis_and_distance_bands(self):
|
||||
state = CanLeadDataState()
|
||||
|
||||
for _ in range(state.LEAD_HYSTERESIS_FRAMES - 1):
|
||||
lead_data = state.update(18.0, -0.5, True)
|
||||
assert not lead_data.lead_visible
|
||||
assert lead_data.object_gap == 0
|
||||
|
||||
lead_data = state.update(18.0, -0.5, True)
|
||||
assert lead_data.lead_visible
|
||||
assert lead_data.object_gap == 2
|
||||
assert lead_data.object_rel_gap == 2
|
||||
|
||||
for _ in range(state.LEAD_HYSTERESIS_FRAMES):
|
||||
lead_data = state.update(32.0, 0.5, True)
|
||||
assert lead_data.object_gap == 5
|
||||
assert lead_data.object_rel_gap == 1
|
||||
|
||||
def test_can_acc_commands_follow_sonata_main_cruise_state(self):
|
||||
CP = CarParams.new_message()
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, gen_empty_fingerprint
|
||||
from opendbc.car.hyundai.carcontroller import CarController
|
||||
from opendbc.car.hyundai.carstate import CarState
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.values import CAR, DBC, HyundaiSafetyFlags
|
||||
from opendbc.car.structs import CarControl
|
||||
|
||||
|
||||
def ray_fingerprint(sensor_length=6, lfa_length=8):
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
fingerprint[0][0x201] = sensor_length
|
||||
fingerprint[0][0x391] = 8
|
||||
fingerprint[2][0x485] = lfa_length
|
||||
return fingerprint
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("candidate", "fingerprint", "has_pedal"), [
|
||||
(CAR.KIA_RAY_EV, ray_fingerprint(), True),
|
||||
(CAR.KIA_RAY_EV, ray_fingerprint(sensor_length=8), False),
|
||||
(CAR.KIA_RAY_EV, ray_fingerprint(lfa_length=4), False),
|
||||
(CAR.HYUNDAI_KONA_EV_NON_SCC, ray_fingerprint(), False),
|
||||
])
|
||||
def test_ray_pedal_fingerprint_isolation(candidate, fingerprint, has_pedal):
|
||||
CP = CarInterface.get_params(candidate, fingerprint, [], False, False, False, None)
|
||||
assert CP.enableGasInterceptorDEPRECATED is has_pedal
|
||||
assert CP.openpilotLongitudinalControl is has_pedal
|
||||
if has_pedal:
|
||||
assert not CP.pcmCruise
|
||||
assert CP.safetyConfigs[-1].safetyParam == 0x9405
|
||||
assert CP.minEnableSpeed == -1.0
|
||||
assert not CP.autoResumeSng
|
||||
FPCP = CarInterface.get_starpilot_params(candidate, fingerprint, [], CP, SimpleNamespace())
|
||||
assert FPCP.canUsePedal
|
||||
assert not FPCP.pcmCruiseSpeed
|
||||
assert not FPCP.redneckCruiseAvailable
|
||||
else:
|
||||
assert CP.pcmCruise
|
||||
assert not (CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG)
|
||||
|
||||
|
||||
def test_ray_pedal_safety_signature_is_unique_across_hyundai_platforms():
|
||||
for candidate in CAR:
|
||||
for alpha_long in (False, True):
|
||||
CP = CarInterface.get_params(candidate, ray_fingerprint(), [],
|
||||
alpha_long, False, False, None)
|
||||
has_ray_signature = (CP.safetyConfigs[-1].safetyParam & ~(32 | 128 | 2048)) == 0x9405
|
||||
assert has_ray_signature is (candidate == CAR.KIA_RAY_EV)
|
||||
assert CP.enableGasInterceptorDEPRECATED is (candidate == CAR.KIA_RAY_EV)
|
||||
|
||||
|
||||
def test_ray_pedal_parser_validates_actual_route_frames():
|
||||
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
|
||||
parser = CarState(CP, None).get_can_parsers(CP)[Bus.party]
|
||||
assert parser.dbc_name == "hyundai_kia_ray_pedal"
|
||||
# Consecutive bus-0 GAS_SENSOR frames from Sept. 15 Ray rlog segment 4.
|
||||
samples = [bytes.fromhex(s) for s in (
|
||||
"01f403d55de8", "01f603d55ef1", "01f403d55f51", "01f603d3503f",
|
||||
"01f903d551ab", "01f903d552a4", "01f703d55370",
|
||||
)]
|
||||
for idx, dat in enumerate(samples):
|
||||
parser.update([(1_000_000_000 + idx * 20_000_000, [(0x201, dat, 0)])])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["GAS_SENSOR"]["STATE"] == 5 # FAULT_TIMEOUT: no 0x200 was sent
|
||||
|
||||
prior = parser.vl_raw["GAS_SENSOR"]
|
||||
bad = bytearray(samples[-1])
|
||||
bad[-1] ^= 1
|
||||
parser.update([(1_160_000_000, [(0x201, bytes(bad), 0)])])
|
||||
assert parser.vl_raw["GAS_SENSOR"] == prior
|
||||
|
||||
|
||||
def test_ray_pedal_fault_clears_only_with_healthy_sensor_state():
|
||||
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
|
||||
state = CarState(CP, None)
|
||||
parsers = state.get_can_parsers(CP)
|
||||
packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
sensor = packer.make_can_msg("GAS_SENSOR", 0, {
|
||||
"INTERCEPTOR_GAS": 0, "INTERCEPTOR_GAS2": 0,
|
||||
"STATE": 0, "COUNTER_PEDAL": 1,
|
||||
})
|
||||
for parser in parsers.values():
|
||||
parser.update([(1_000_000_000, [sensor])])
|
||||
ret, _ = state.update(parsers, SimpleNamespace())
|
||||
assert state.ray_pedal_valid
|
||||
assert state.ray_pedal_state == 0
|
||||
assert not ret.accFaulted
|
||||
|
||||
|
||||
def test_ray_driver_override_uses_physical_interceptor_tracks():
|
||||
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
|
||||
state = CarState(CP, None)
|
||||
parsers = state.get_can_parsers(CP)
|
||||
native_gas = (0x371, bytes.fromhex("004e008000ae0700"), 0)
|
||||
physical_rest = (0x201, bytes.fromhex("010801f30cef"), 0)
|
||||
for parser in parsers.values():
|
||||
parser.update([(1_000_000_000, [native_gas, physical_rest])])
|
||||
ret, _ = state.update(parsers, SimpleNamespace())
|
||||
assert state.ray_pedal_valid
|
||||
assert not ret.gasPressed
|
||||
|
||||
packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
physical_press = packer.make_can_msg("GAS_SENSOR", 0, {
|
||||
"INTERCEPTOR_GAS": (310 - 264) * 0.672,
|
||||
"INTERCEPTOR_GAS2": (593 - 497) * 0.332,
|
||||
"STATE": 0, "COUNTER_PEDAL": 13,
|
||||
})
|
||||
for parser in parsers.values():
|
||||
parser.update([(1_020_000_000, [physical_press])])
|
||||
ret, _ = state.update(parsers, SimpleNamespace())
|
||||
assert ret.gasPressed
|
||||
|
||||
|
||||
def test_ray_without_pedal_keeps_native_gas_detection():
|
||||
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(sensor_length=8), [], False, False, False, None)
|
||||
assert not CP.enableGasInterceptorDEPRECATED
|
||||
state = CarState(CP, None)
|
||||
parsers = state.get_can_parsers(CP)
|
||||
assert Bus.party not in parsers
|
||||
native_gas = (0x371, bytes.fromhex("004e008000ae0700"), 0)
|
||||
for parser in parsers.values():
|
||||
parser.update([(1_000_000_000, [native_gas])])
|
||||
ret, _ = state.update(parsers, SimpleNamespace())
|
||||
assert ret.gasPressed
|
||||
|
||||
|
||||
@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=speed, gasPressed=False, brakePressed=False,
|
||||
cruiseState=SimpleNamespace(enabled=False)),
|
||||
ray_pedal_valid=True, ray_pedal_state=5, is_metric=True,
|
||||
)
|
||||
CC = SimpleNamespace(
|
||||
enabled=True, longActive=True, latActive=True,
|
||||
cruiseControl=SimpleNamespace(cancel=False, resume=False, override=False),
|
||||
)
|
||||
hud = SimpleNamespace(
|
||||
visualAlert=CarControl.HUDControl.VisualAlert.none,
|
||||
setSpeed=20.0,
|
||||
leftLaneVisible=True, rightLaneVisible=True,
|
||||
leftLaneDepart=False, rightLaneDepart=False,
|
||||
)
|
||||
actuators = SimpleNamespace(longControlState=CarControl.Actuators.LongControlState.pid)
|
||||
|
||||
def pedal_msg(accel, frame):
|
||||
controller.frame = frame
|
||||
messages = controller.create_can_msgs(True, 0, False, 0.0, accel, False,
|
||||
hud, actuators, CS, CC, 2, 0)
|
||||
return next(dat for addr, dat, bus in messages if addr == 0x200 and bus == 0)
|
||||
|
||||
assert pedal_msg(2.0, 0)[:4] == bytes(4) # fault timeout: heartbeat only
|
||||
CS.ray_pedal_state = 0
|
||||
assert pedal_msg(2.0, 4)[:4] != bytes(4)
|
||||
CS.out.gasPressed = True
|
||||
assert pedal_msg(2.0, 8)[:4] == bytes(4)
|
||||
CS.out.gasPressed = False
|
||||
assert pedal_msg(-1.0, 12)[:4] == bytes(4) # decel = EV lift/regen, not gas
|
||||
CS.out.cruiseState.enabled = True
|
||||
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] & 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),
|
||||
@@ -1218,6 +1220,13 @@ CANFD_ALT_BUTTONS_RESUME_CAR = {CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_
|
||||
CANFD_CORNER_RADAR_BSM_CAR = {CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9}
|
||||
CANFD_RADAR_LIVE_LONGITUDINAL_CAR = {
|
||||
CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6, CAR.KIA_EV9, CAR.GENESIS_GV60_EV_1ST_GEN,
|
||||
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
|
||||
}
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR = {
|
||||
CAR.HYUNDAI_IONIQ_5_PE,
|
||||
CAR.HYUNDAI_IONIQ_6,
|
||||
CAR.KIA_EV9,
|
||||
CAR.GENESIS_GV60_EV_1ST_GEN,
|
||||
}
|
||||
RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | {
|
||||
CAR.HYUNDAI_IONIQ,
|
||||
|
||||
@@ -109,6 +109,7 @@ class RadarInterfaceBase(ABC):
|
||||
self.CP = CP
|
||||
self.rcp = None
|
||||
self.pts: dict[int, structs.RadarData.RadarPoint] = {}
|
||||
self.track_id: int = 0
|
||||
self.frame = 0
|
||||
|
||||
def update(self, can_packets: list[tuple[int, list[CanData]]]) -> structs.RadarDataT | None:
|
||||
@@ -232,6 +233,9 @@ class CarInterfaceBase(ABC):
|
||||
fp_ret.flags |= int(HondaStarPilotFlags.HAS_CAMERA_MESSAGES)
|
||||
|
||||
elif platform in HYUNDAI:
|
||||
if candidate == HYUNDAI.KIA_RAY_EV and CP.enableGasInterceptorDEPRECATED:
|
||||
fp_ret.canUsePedal = True
|
||||
fp_ret.pcmCruiseSpeed = False
|
||||
if candidate in CANFD_CAR:
|
||||
hda2 = Ecu.adas in [fw.ecu for fw in car_fw]
|
||||
CAN = CanBus(None, fingerprint, bool(CP.flags & HyundaiFlags.CANFD_LKA_STEERING))
|
||||
@@ -240,13 +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)
|
||||
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
|
||||
@@ -269,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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,21 +17,11 @@ _SNG_ACC_MIN_DIST = 3
|
||||
_SNG_ACC_MAX_DIST = 4.5
|
||||
_LEGACY_2025_MADS_MIN_SPEED = 0.44704
|
||||
_LEGACY_2025_MADS_MAX_STEER_ANGLE = 120.0
|
||||
_LEGACY_2025_OVERRIDE_HOLD_FRAMES = 10
|
||||
_LEGACY_2025_REENGAGE_SETTLE_FRAMES = 8
|
||||
_LEGACY_2025_REENGAGE_MAX_STEER_RATE = 2.0
|
||||
_LEGACY_2025_REENGAGE_MAX_ANGLE_DELTA = 1.0
|
||||
_LEGACY_2025_RECLAIM_FRAMES = 36
|
||||
_LEGACY_2025_RECLAIM_EXPONENT = 2.5
|
||||
_ANGLE_OVERRIDE_CONFIRM_FRAMES = 2
|
||||
_ANGLE_OVERRIDE_HOLD_FRAMES = 10
|
||||
_ANGLE_REENGAGE_SETTLE_FRAMES = 8
|
||||
_ANGLE_REENGAGE_MAX_STEER_RATE = 2.0
|
||||
_ANGLE_REENGAGE_MAX_ANGLE_DELTA = 1.0
|
||||
_ANGLE_RECLAIM_FRAMES = 36
|
||||
_ANGLE_RECLAIM_EXPONENT = 2.5
|
||||
_ANGLE_MADS_MIN_SPEED = 0.44704
|
||||
_ANGLE_MADS_MAX_STEER_ANGLE = 120.0
|
||||
_ASCENT_AOL_ARM_FRAMES = 30
|
||||
_STOP_START_STARTUP_DELAY_FRAMES = 100
|
||||
# StarPilot's first populated toggle message can arrive several seconds after
|
||||
# the car controller starts while fingerprinting and settings settle.
|
||||
@@ -53,20 +44,11 @@ class CarController(CarControllerBase):
|
||||
self.apply_steer_last = 0
|
||||
self.driver_override = False
|
||||
self.angle_override_confirm_frames = 0
|
||||
self.legacy_2025_lkas_active = False
|
||||
self.legacy_2025_handoff_active = False
|
||||
self.legacy_2025_override_hold_frames = 0
|
||||
self.legacy_2025_reengage_settle_frames = 0
|
||||
self.legacy_2025_reengage_reference_angle = 0.0
|
||||
self.legacy_2025_reclaim_frames = 0
|
||||
self.legacy_2025_reclaim_start_angle = 0.0
|
||||
self.angle_lkas_active = False
|
||||
self.angle_handoff_active = False
|
||||
self.angle_override_hold_frames = 0
|
||||
self.angle_reengage_settle_frames = 0
|
||||
self.angle_reengage_reference_angle = 0.0
|
||||
self.angle_reclaim_frames = 0
|
||||
self.angle_reclaim_start_angle = 0.0
|
||||
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
|
||||
@@ -77,7 +59,7 @@ class CarController(CarControllerBase):
|
||||
self.angle_bus = CanBus.angle_for_cp(CP)
|
||||
self.status_bus = CanBus.camera if CP.flags & SubaruFlags.D_PLATFORM_CAMERA else CanBus.main
|
||||
|
||||
if CP.flags & SubaruFlags.LKAS_ANGLE:
|
||||
if CP.flags & SubaruFlags.LKAS_ANGLE and CP.carFingerprint != CAR.SUBARU_OUTBACK_2023:
|
||||
self.VM = VehicleModel(get_safety_CP())
|
||||
|
||||
self.prev_close_distance = 0
|
||||
@@ -90,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.
|
||||
@@ -146,81 +129,10 @@ class CarController(CarControllerBase):
|
||||
self.stop_start_counter = (self.stop_start_counter + 1) % 0x10
|
||||
return msg
|
||||
|
||||
def _reset_legacy_2025_handoff(self):
|
||||
self.driver_override = False
|
||||
self.angle_override_confirm_frames = 0
|
||||
self.legacy_2025_handoff_active = False
|
||||
self.legacy_2025_override_hold_frames = 0
|
||||
self.legacy_2025_reengage_settle_frames = 0
|
||||
self.legacy_2025_reengage_reference_angle = 0.0
|
||||
self.legacy_2025_reclaim_frames = 0
|
||||
self.legacy_2025_reclaim_start_angle = 0.0
|
||||
|
||||
def _legacy_2025_manual_handoff(self, CS, lkas_available):
|
||||
if not lkas_available:
|
||||
self._reset_legacy_2025_handoff()
|
||||
return False
|
||||
|
||||
driver_override = self._update_angle_driver_override(CS)
|
||||
if driver_override:
|
||||
self.legacy_2025_handoff_active = True
|
||||
self.legacy_2025_override_hold_frames = _LEGACY_2025_OVERRIDE_HOLD_FRAMES
|
||||
self.legacy_2025_reengage_settle_frames = 0
|
||||
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
self.legacy_2025_reclaim_frames = 0
|
||||
return True
|
||||
|
||||
if not self.legacy_2025_handoff_active and not self.legacy_2025_lkas_active and \
|
||||
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _LEGACY_2025_REENGAGE_MAX_STEER_RATE:
|
||||
self.legacy_2025_handoff_active = True
|
||||
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
|
||||
if not self.legacy_2025_handoff_active:
|
||||
return False
|
||||
|
||||
if self.legacy_2025_override_hold_frames > 0:
|
||||
self.legacy_2025_override_hold_frames -= 1
|
||||
if self.legacy_2025_override_hold_frames == 0:
|
||||
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
return True
|
||||
|
||||
wheel_stable = abs(getattr(CS.out, "steeringRateDeg", 0.0)) <= _LEGACY_2025_REENGAGE_MAX_STEER_RATE and \
|
||||
abs(CS.out.steeringAngleDeg - self.legacy_2025_reengage_reference_angle) <= _LEGACY_2025_REENGAGE_MAX_ANGLE_DELTA
|
||||
if wheel_stable:
|
||||
self.legacy_2025_reengage_settle_frames += 1
|
||||
else:
|
||||
self.legacy_2025_reengage_settle_frames = 0
|
||||
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
|
||||
if self.legacy_2025_reengage_settle_frames < _LEGACY_2025_REENGAGE_SETTLE_FRAMES:
|
||||
return True
|
||||
|
||||
self.legacy_2025_handoff_active = False
|
||||
self.legacy_2025_reengage_settle_frames = 0
|
||||
self.legacy_2025_reclaim_frames = _LEGACY_2025_RECLAIM_FRAMES
|
||||
self.legacy_2025_reclaim_start_angle = CS.out.steeringAngleDeg
|
||||
return True
|
||||
|
||||
def _legacy_2025_reclaim_target(self, target_angle):
|
||||
if self.legacy_2025_reclaim_frames <= 0:
|
||||
return target_angle
|
||||
|
||||
progress = (_LEGACY_2025_RECLAIM_FRAMES - self.legacy_2025_reclaim_frames + 1) / _LEGACY_2025_RECLAIM_FRAMES
|
||||
eased_progress = progress ** _LEGACY_2025_RECLAIM_EXPONENT
|
||||
target_angle = self.legacy_2025_reclaim_start_angle + eased_progress * \
|
||||
(target_angle - self.legacy_2025_reclaim_start_angle)
|
||||
self.legacy_2025_reclaim_frames -= 1
|
||||
return target_angle
|
||||
|
||||
def _reset_angle_handoff(self):
|
||||
self.driver_override = False
|
||||
self.angle_override_confirm_frames = 0
|
||||
self.angle_handoff_active = False
|
||||
self.angle_override_hold_frames = 0
|
||||
self.angle_reengage_settle_frames = 0
|
||||
self.angle_reengage_reference_angle = 0.0
|
||||
self.angle_reclaim_frames = 0
|
||||
self.angle_reclaim_start_angle = 0.0
|
||||
|
||||
def _angle_manual_handoff(self, CS, lat_active):
|
||||
if not lat_active:
|
||||
@@ -228,44 +140,23 @@ class CarController(CarControllerBase):
|
||||
return False
|
||||
|
||||
driver_override = self._update_angle_driver_override(CS)
|
||||
steering_rate = abs(getattr(CS.out, "steeringRateDeg", 0.0))
|
||||
if driver_override:
|
||||
self.angle_handoff_active = True
|
||||
self.angle_override_hold_frames = _ANGLE_OVERRIDE_HOLD_FRAMES
|
||||
self.angle_reengage_settle_frames = 0
|
||||
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
self.angle_reclaim_frames = 0
|
||||
return True
|
||||
|
||||
if not self.angle_handoff_active and not self.angle_lkas_active and \
|
||||
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _ANGLE_REENGAGE_MAX_STEER_RATE:
|
||||
if self.angle_handoff_active:
|
||||
if steering_rate > _ANGLE_REENGAGE_MAX_STEER_RATE:
|
||||
return True
|
||||
|
||||
self.angle_handoff_active = False
|
||||
return True
|
||||
|
||||
if not self.angle_lkas_active and steering_rate > _ANGLE_REENGAGE_MAX_STEER_RATE:
|
||||
self.angle_handoff_active = True
|
||||
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
|
||||
if not self.angle_handoff_active:
|
||||
return False
|
||||
|
||||
if self.angle_override_hold_frames > 0:
|
||||
self.angle_override_hold_frames -= 1
|
||||
if self.angle_override_hold_frames == 0:
|
||||
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
return True
|
||||
|
||||
wheel_stable = abs(getattr(CS.out, "steeringRateDeg", 0.0)) <= _ANGLE_REENGAGE_MAX_STEER_RATE and \
|
||||
abs(CS.out.steeringAngleDeg - self.angle_reengage_reference_angle) <= _ANGLE_REENGAGE_MAX_ANGLE_DELTA
|
||||
if wheel_stable:
|
||||
self.angle_reengage_settle_frames += 1
|
||||
else:
|
||||
self.angle_reengage_settle_frames = 0
|
||||
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
|
||||
|
||||
if self.angle_reengage_settle_frames < _ANGLE_REENGAGE_SETTLE_FRAMES:
|
||||
return True
|
||||
|
||||
self.angle_handoff_active = False
|
||||
self.angle_reengage_settle_frames = 0
|
||||
self.angle_reclaim_frames = _ANGLE_RECLAIM_FRAMES
|
||||
self.angle_reclaim_start_angle = CS.out.steeringAngleDeg
|
||||
return True
|
||||
return False
|
||||
|
||||
def _update_angle_driver_override(self, CS):
|
||||
"""Debounce the higher-confidence raw torque override signal for angle cars."""
|
||||
@@ -283,16 +174,13 @@ class CarController(CarControllerBase):
|
||||
|
||||
return self.driver_override
|
||||
|
||||
def _angle_reclaim_target(self, target_angle):
|
||||
if self.angle_reclaim_frames <= 0:
|
||||
return target_angle
|
||||
def _ascent_aol_ready(self, ready):
|
||||
if not ready:
|
||||
self.ascent_aol_arm_frames = 0
|
||||
return False
|
||||
|
||||
progress = (_ANGLE_RECLAIM_FRAMES - self.angle_reclaim_frames + 1) / _ANGLE_RECLAIM_FRAMES
|
||||
eased_progress = progress ** _ANGLE_RECLAIM_EXPONENT
|
||||
target_angle = self.angle_reclaim_start_angle + eased_progress * \
|
||||
(target_angle - self.angle_reclaim_start_angle)
|
||||
self.angle_reclaim_frames -= 1
|
||||
return target_angle
|
||||
self.ascent_aol_arm_frames = min(self.ascent_aol_arm_frames + 1, _ASCENT_AOL_ARM_FRAMES)
|
||||
return self.ascent_aol_arm_frames >= _ASCENT_AOL_ARM_FRAMES
|
||||
|
||||
def lateral_angle(self, CC, CS):
|
||||
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
|
||||
@@ -302,12 +190,11 @@ class CarController(CarControllerBase):
|
||||
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
|
||||
CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill
|
||||
|
||||
manual_handoff = self._legacy_2025_manual_handoff(CS, lkas_available)
|
||||
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
|
||||
lkas_active = lkas_available and not manual_handoff
|
||||
|
||||
steer_target = self._legacy_2025_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg
|
||||
apply_steer = apply_std_steer_angle_limits(
|
||||
steer_target,
|
||||
CC.actuators.steeringAngleDeg,
|
||||
self.apply_steer_last,
|
||||
CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg,
|
||||
@@ -315,42 +202,44 @@ class CarController(CarControllerBase):
|
||||
self.p.LEGACY_2025_ANGLE_LIMITS,
|
||||
)
|
||||
self.apply_steer_last = apply_steer
|
||||
self.legacy_2025_lkas_active = lkas_active
|
||||
self.angle_lkas_active = lkas_active
|
||||
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
|
||||
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
|
||||
CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill
|
||||
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023:
|
||||
if mads_only:
|
||||
cruise_available = getattr(getattr(CS.out, "cruiseState", None), "available", True)
|
||||
lkas_available = self._ascent_aol_ready(lkas_available and cruise_available)
|
||||
else:
|
||||
self.ascent_aol_arm_frames = _ASCENT_AOL_ARM_FRAMES if lkas_available else 0
|
||||
|
||||
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
|
||||
if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
|
||||
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
|
||||
|
||||
steer_target = self._angle_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg
|
||||
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023:
|
||||
apply_steer = apply_std_steer_angle_limits(
|
||||
steer_target,
|
||||
self.apply_steer_last,
|
||||
CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg,
|
||||
lkas_active,
|
||||
self.p.FIXED_ANGLE_LIMITS,
|
||||
)
|
||||
else:
|
||||
apply_steer = apply_steer_angle_limits_vm(
|
||||
steer_target,
|
||||
self.apply_steer_last,
|
||||
CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg,
|
||||
lkas_active,
|
||||
self.p,
|
||||
self.VM,
|
||||
)
|
||||
apply_steer = apply_std_steer_angle_limits(
|
||||
CC.actuators.steeringAngleDeg,
|
||||
self.apply_steer_last,
|
||||
CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg,
|
||||
lkas_active,
|
||||
self.p.FIXED_ANGLE_LIMITS,
|
||||
)
|
||||
self.apply_steer_last = apply_steer
|
||||
self.angle_lkas_active = lkas_active
|
||||
return subarucan.create_steering_control_angle(self.packer, apply_steer, lkas_active, self.angle_bus)
|
||||
@@ -365,9 +254,8 @@ class CarController(CarControllerBase):
|
||||
lat_active = lkas_available and not self.driver_override and not manual_handoff
|
||||
if lat_active and not self.angle_lkas_active:
|
||||
self.apply_steer_last = CS.out.steeringAngleDeg
|
||||
steer_target = self._angle_reclaim_target(CC.actuators.steeringAngleDeg) if lat_active else CC.actuators.steeringAngleDeg
|
||||
apply_steer = apply_steer_angle_limits_vm(
|
||||
steer_target,
|
||||
CC.actuators.steeringAngleDeg,
|
||||
self.apply_steer_last,
|
||||
CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg,
|
||||
@@ -408,6 +296,11 @@ class CarController(CarControllerBase):
|
||||
|
||||
return subarucan.create_steering_control(self.packer, apply_torque, apply_steer_req)
|
||||
|
||||
def _lkas_status_active(self, CC):
|
||||
if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
||||
return self.angle_lkas_active
|
||||
return CC.latActive
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
actuators = CC.actuators
|
||||
hud_control = CC.hudControl
|
||||
@@ -423,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:
|
||||
@@ -484,9 +384,11 @@ class CarController(CarControllerBase):
|
||||
CC.longActive, hud_control.leadVisible,
|
||||
self.status_bus))
|
||||
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.latActive, hud_control.visualAlert,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart, self.status_bus))
|
||||
can_sends.append(subarucan.create_es_lkas_state(
|
||||
self.packer, self.frame // 10, CS.es_lkas_state_msg, self._lkas_status_active(CC), hud_control.visualAlert,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart, self.status_bus,
|
||||
))
|
||||
|
||||
if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT:
|
||||
can_sends.append(subarucan.create_es_infotainment(self.packer, self.frame // 10, CS.es_infotainment_msg,
|
||||
@@ -510,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))
|
||||
|
||||
@@ -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"])
|
||||
@@ -85,14 +90,14 @@ class CarState(CarStateBase):
|
||||
|
||||
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
|
||||
ret.steeringAngleDeg = cp_angle.vl["Steering_2"]["Steering_Angle"]
|
||||
steering_updated = len(cp_angle.vl_all["Steering_2"]["Steering_Angle"]) > 0
|
||||
steering_counter = cp_angle.vl["Steering_2"]["COUNTER"]
|
||||
else:
|
||||
ret.steeringAngleDeg = cp.vl["Steering_Torque"]["Steering_Angle"]
|
||||
steering_updated = len(cp.vl_all["Steering_Torque"]["Steering_Angle"]) > 0
|
||||
steering_counter = cp.vl["Steering_Torque"].get("COUNTER", 0)
|
||||
|
||||
if not (self.CP.flags & SubaruFlags.PREGLOBAL):
|
||||
# ideally we get this from the car, but unclear if it exists. diagnostic software doesn't even have it
|
||||
ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, steering_updated)
|
||||
ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, steering_counter)
|
||||
|
||||
ret.steeringTorque = cp_angle.vl["Steering_Torque"]["Steer_Torque_Sensor"]
|
||||
ret.steeringTorqueEps = cp_angle.vl["Steering_Torque"]["Steer_Torque_Output"]
|
||||
@@ -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,7 +42,9 @@ 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 in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
|
||||
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
|
||||
|
||||
ret.steerLimitTimer = 0.4
|
||||
|
||||
@@ -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)
|
||||
@@ -8,7 +8,7 @@ from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, fw_versions, gen_empty_fingerprint, structs
|
||||
from opendbc.car.fw_query_definitions import StdQueries
|
||||
from opendbc.car.subaru import subarucan
|
||||
from opendbc.car.subaru.carcontroller import CarController
|
||||
from opendbc.car.subaru.carcontroller import CarController, _ASCENT_AOL_ARM_FRAMES
|
||||
from opendbc.car.subaru.carstate import CarState
|
||||
from opendbc.car.subaru.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.fw_versions import match_fw_to_car
|
||||
@@ -244,7 +244,7 @@ def test_outback_2023_uses_d_platform_bus_layout():
|
||||
assert CP.flags & SubaruFlags.D_PLATFORM
|
||||
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
|
||||
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
|
||||
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.LEGACY_2025_ANGLE_LIMITS)
|
||||
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.FIXED_ANGLE_LIMITS
|
||||
assert CanBus.main_for_cp(CP) == CanBus.alt
|
||||
assert CanBus.angle_for_cp(CP) == CanBus.main
|
||||
assert parsers[Bus.pt].bus == CanBus.alt
|
||||
@@ -414,7 +414,7 @@ def test_legacy_2025_engagement_continues_from_last_sent_angle():
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(0.47)
|
||||
|
||||
|
||||
def test_legacy_2025_waits_for_manual_steering_to_settle_before_reengaging():
|
||||
def test_legacy_2025_reengages_immediately_after_manual_steering_stops():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(
|
||||
@@ -444,42 +444,20 @@ def test_legacy_2025_waits_for_manual_steering_to_settle_before_reengaging():
|
||||
CS.out.steeringPressed = False
|
||||
CS.out.steeringTorque = 0.0
|
||||
CS.out.steeringAngleDeg = -113.78
|
||||
CS.out.steeringRateDeg = 0.0
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(2, [msg])])
|
||||
parser.update([(3, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
for i in range(9):
|
||||
CS.out.steeringAngleDeg += 0.5
|
||||
CS.out.steeringRateDeg = 20.0
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(3 + i, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
for i in range(6):
|
||||
if i % 2:
|
||||
CS.out.steeringAngleDeg += 0.5
|
||||
CS.out.steeringRateDeg = 0.0 if i % 2 == 0 else 20.0
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(12 + i, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
CS.out.steeringRateDeg = 0.0
|
||||
for i in range(8):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(18 + i, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
|
||||
measured_angle = CS.out.steeringAngleDeg
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(26, [msg])])
|
||||
parser.update([(4, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
assert abs(parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] - measured_angle) < 0.1
|
||||
assert -113.78 < parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] < -100.0
|
||||
|
||||
|
||||
def test_legacy_2025_manual_handoff_reclaim_is_gradual():
|
||||
def test_legacy_2025_manual_handoff_reentry_uses_normal_angle_limits():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(
|
||||
@@ -508,22 +486,24 @@ def test_legacy_2025_manual_handoff_reclaim_is_gradual():
|
||||
CS.out.steeringPressed = False
|
||||
CS.out.steeringTorque = 0.0
|
||||
CS.out.steeringRateDeg = 0.0
|
||||
for i in range(19):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(2 + i, [msg])])
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(3, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(4, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
first_reclaim_angle = parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"]
|
||||
assert first_reclaim_angle == pytest.approx(CS.out.steeringAngleDeg, abs=0.1)
|
||||
first_reentry_angle = parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"]
|
||||
assert CC.actuators.steeringAngleDeg < first_reentry_angle < CS.out.steeringAngleDeg
|
||||
|
||||
reclaim_angles = []
|
||||
reentry_angles = []
|
||||
for i in range(6):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(20 + i, [msg])])
|
||||
reclaim_angles.append(parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"])
|
||||
reentry_angles.append(parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"])
|
||||
|
||||
assert all(reclaim_angles[i] >= reclaim_angles[i + 1] for i in range(len(reclaim_angles) - 1))
|
||||
assert reclaim_angles[-1] > CC.actuators.steeringAngleDeg
|
||||
assert all(reentry_angles[i] >= reentry_angles[i + 1] for i in range(len(reentry_angles) - 1))
|
||||
assert reentry_angles[-1] > CC.actuators.steeringAngleDeg
|
||||
|
||||
|
||||
def test_ascent_2023_uses_gen2_angle_bus_layout():
|
||||
@@ -546,6 +526,25 @@ def test_ascent_2023_uses_gen2_angle_bus_layout():
|
||||
assert controller.status_bus == CanBus.main
|
||||
|
||||
|
||||
def test_ascent_steering_rate_retains_last_can_sample():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
car_state = CarState(CP, None)
|
||||
parsers = car_state.get_can_parsers(CP)
|
||||
toggles = SimpleNamespace(subaru_sng=False)
|
||||
|
||||
parsers[Bus.pt].vl["Steering_2"]["Steering_Angle"] = 1.0
|
||||
parsers[Bus.pt].vl["Steering_2"]["COUNTER"] = 1
|
||||
car_state.update(parsers, toggles)
|
||||
|
||||
parsers[Bus.pt].vl["Steering_2"]["Steering_Angle"] = 2.0
|
||||
parsers[Bus.pt].vl["Steering_2"]["COUNTER"] = 2
|
||||
state, _ = car_state.update(parsers, toggles)
|
||||
assert state.steeringRateDeg == pytest.approx(50.0)
|
||||
|
||||
state, _ = car_state.update(parsers, toggles)
|
||||
assert state.steeringRateDeg == pytest.approx(50.0)
|
||||
|
||||
|
||||
def test_other_angle_platforms_keep_existing_bus_layout():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
|
||||
parsers = CarState.get_can_parsers(CP)
|
||||
@@ -622,8 +621,9 @@ def test_angle_controller_blocks_low_speed_mads_engagement():
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
|
||||
|
||||
def test_ascent_angle_controller_uses_fixed_angle_rate_limits():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
@pytest.mark.parametrize("platform", (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023))
|
||||
def test_angle_controller_uses_fixed_angle_rate_limits(platform):
|
||||
CP = CarInterface.get_non_essential_params(platform)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-14.88))
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
@@ -643,8 +643,8 @@ def test_ascent_angle_controller_uses_fixed_angle_rate_limits():
|
||||
assert CS.out.steeringAngleDeg < parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] < -25.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023))
|
||||
def test_angle_controller_yields_until_manual_steering_settles(platform):
|
||||
def test_ascent_angle_controller_reengages_immediately_after_manual_steering_stops():
|
||||
platform = CAR.SUBARU_ASCENT_2023
|
||||
CP = CarInterface.get_non_essential_params(platform)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-10.0))
|
||||
@@ -671,18 +671,40 @@ def test_angle_controller_yields_until_manual_steering_settles(platform):
|
||||
CS.out.steeringTorque = 0.0
|
||||
CS.out.steeringAngleDeg = -17.91
|
||||
CS.out.steeringRateDeg = 0.0
|
||||
for i in range(18):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(2 + i, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(3, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(20, [msg])])
|
||||
parser.update([(4, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg, abs=0.1)
|
||||
assert CS.out.steeringAngleDeg < parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] < CC.actuators.steeringAngleDeg
|
||||
|
||||
|
||||
def test_ascent_angle_controller_blocks_parking_lot_aol_engagement():
|
||||
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)
|
||||
CC = SimpleNamespace(enabled=False, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-206.12))
|
||||
@@ -692,6 +714,7 @@ def test_ascent_angle_controller_blocks_parking_lot_aol_engagement():
|
||||
steeringRateDeg=96.0,
|
||||
steeringTorque=7.0,
|
||||
steeringPressed=False,
|
||||
cruiseState=SimpleNamespace(available=True),
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
standstill=False,
|
||||
))
|
||||
@@ -704,21 +727,77 @@ def test_ascent_angle_controller_blocks_parking_lot_aol_engagement():
|
||||
|
||||
CS.out.steeringAngleDeg = -100.0
|
||||
CS.out.steeringRateDeg = 0.0
|
||||
for frame in range(2, _ASCENT_AOL_ARM_FRAMES + 1):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(frame, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(2, [msg])])
|
||||
parser.update([(_ASCENT_AOL_ARM_FRAMES + 1, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
|
||||
CS.out.gearShifter = structs.CarState.GearShifter.reverse
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(3, [msg])])
|
||||
parser.update([(_ASCENT_AOL_ARM_FRAMES + 2, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
|
||||
def test_lkas_hud_state_uses_lateral_active():
|
||||
def test_ascent_aol_does_not_arm_before_cruise_main_is_available():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(enabled=False, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=5.0))
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=10.0,
|
||||
steeringAngleDeg=0.0,
|
||||
steeringRateDeg=0.0,
|
||||
steeringTorque=0.0,
|
||||
steeringPressed=False,
|
||||
cruiseState=SimpleNamespace(available=False),
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
standstill=False,
|
||||
))
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
|
||||
|
||||
for frame in range(_ASCENT_AOL_ARM_FRAMES):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(frame + 1, [msg])])
|
||||
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert controller.ascent_aol_arm_frames == 0
|
||||
|
||||
CS.out.cruiseState.available = True
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(_ASCENT_AOL_ARM_FRAMES + 1, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert controller.ascent_aol_arm_frames == 1
|
||||
|
||||
|
||||
def test_ascent_angle_controller_does_not_delay_normal_engagement():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=5.0))
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=10.0,
|
||||
steeringAngleDeg=0.0,
|
||||
steeringRateDeg=0.0,
|
||||
steeringTorque=0.0,
|
||||
steeringPressed=False,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
standstill=False,
|
||||
))
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(1, [msg])])
|
||||
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
|
||||
|
||||
def test_lkas_hud_state_uses_angle_request_state():
|
||||
update_source = inspect.getsource(CarController.update)
|
||||
|
||||
assert "create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.latActive" in update_source
|
||||
assert "CS.es_lkas_state_msg, self._lkas_status_active(CC), hud_control.visualAlert" in update_source
|
||||
assert "create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.enabled" not in update_source
|
||||
|
||||
|
||||
@@ -736,3 +815,136 @@ def test_lkas_hud_active_bit_follows_lateral_state(enabled, expected):
|
||||
|
||||
assert parser.can_valid
|
||||
assert parser.vl["ES_LKAS_State"]["LKAS_ACTIVE"] == expected
|
||||
|
||||
|
||||
def test_outback_manual_steering_keeps_cooperative_angle_request():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_OUTBACK_2023)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(
|
||||
enabled=False,
|
||||
latActive=True,
|
||||
actuators=SimpleNamespace(steeringAngleDeg=-225.0),
|
||||
)
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=0.9,
|
||||
steeringAngleDeg=-57.0,
|
||||
steeringRateDeg=0.0,
|
||||
steeringTorque=0.0,
|
||||
steeringPressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
standstill=False,
|
||||
))
|
||||
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)
|
||||
parser.update([(frame, [msg])])
|
||||
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
assert CC.actuators.steeringAngleDeg < parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] < CS.out.steeringAngleDeg
|
||||
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)
|
||||
CC = SimpleNamespace(latActive=True)
|
||||
|
||||
assert not controller._lkas_status_active(CC)
|
||||
controller.angle_lkas_active = True
|
||||
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)
|
||||
controller.angle_lkas_active = False
|
||||
|
||||
assert controller._lkas_status_active(SimpleNamespace(latActive=True))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ from opendbc.car.lateral import apply_steer_angle_limits_vm
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.tesla.coop_steering import CooperativeSteeringController
|
||||
from opendbc.car.tesla.teslacan import TeslaCAN
|
||||
from opendbc.car.tesla.teslacan_legacy import TeslaCANRaven
|
||||
from opendbc.car.tesla.preap.carcontroller import PreAPLongController, init_preap_can
|
||||
from opendbc.car.tesla.preap.stock_cc_spoofer import StockCCSpoofer
|
||||
from opendbc.car.tesla.values import CANBUS, CAR, CarControllerParams, TeslaSafetyFlags
|
||||
from opendbc.car.tesla.values import CANBUS, CAR, CarControllerParams, TeslaSafetyFlags, LEGACY_CARS
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
|
||||
def get_safety_CP():
|
||||
@@ -24,6 +25,7 @@ class CarController(CarControllerBase):
|
||||
self.coop_enabled = CP.carFingerprint == CAR.TESLA_MODEL_3 and any(
|
||||
config.safetyParam & TeslaSafetyFlags.COOP_STEERING.value for config in CP.safetyConfigs
|
||||
)
|
||||
self._clear_steering_limit_info()
|
||||
self.packer = CANPacker(dbc_names[Bus.party])
|
||||
self.tesla_can = TeslaCAN(self.packer)
|
||||
self.preap_long = None
|
||||
@@ -38,9 +40,37 @@ class CarController(CarControllerBase):
|
||||
self.stock_cc = StockCCSpoofer()
|
||||
from opendbc.car.tesla.interface import CarInterface
|
||||
self.VM = VehicleModel(CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP))
|
||||
elif CP.carFingerprint in LEGACY_CARS:
|
||||
self.packers = {
|
||||
CANBUS.party: CANPacker(dbc_names[Bus.party]),
|
||||
}
|
||||
self.tesla_can = TeslaCANRaven(self.packers)
|
||||
from opendbc.car.tesla.interface import CarInterface
|
||||
self.VM = VehicleModel(CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW1))
|
||||
|
||||
def _clear_steering_limit_info(self):
|
||||
self.steering_limit_info_valid = False
|
||||
self.model_limit_error_deg = 0.0
|
||||
self.resume_limit_error_deg = 0.0
|
||||
self.cooperative_limit_error_deg = 0.0
|
||||
self.cooperative_offset_deg = 0.0
|
||||
self.steering_limit_mono_time = 0
|
||||
self.combined_limit_error_deg = 0.0
|
||||
|
||||
def get_steering_limit_info(self) -> dict[str, bool | float | int]:
|
||||
return {
|
||||
"valid": self.steering_limit_info_valid,
|
||||
"modelLimitErrorDeg": self.model_limit_error_deg,
|
||||
"resumeLimitErrorDeg": self.resume_limit_error_deg,
|
||||
"cooperativeLimitErrorDeg": self.cooperative_limit_error_deg,
|
||||
"cooperativeOffsetDeg": self.cooperative_offset_deg,
|
||||
"monoTime": self.steering_limit_mono_time,
|
||||
"combinedLimitErrorDeg": self.combined_limit_error_deg,
|
||||
}
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
if self.CP.carFingerprint == CAR.TESLA_MODEL_S_PREAP:
|
||||
self._clear_steering_limit_info()
|
||||
return self._update_preap(CC, CS)
|
||||
|
||||
actuators = CC.actuators
|
||||
@@ -48,8 +78,12 @@ class CarController(CarControllerBase):
|
||||
|
||||
# Preserve the stock controller path unless cooperative steering is explicitly enabled.
|
||||
lat_active = CC.latActive and (not CS.out.steeringDisengage if self.coop_enabled else CS.hands_on_level < 3)
|
||||
if not (self.coop_enabled and lat_active):
|
||||
self._clear_steering_limit_info()
|
||||
|
||||
if self.frame % 2 == 0:
|
||||
requested_angle = actuators.steeringAngleDeg
|
||||
|
||||
# Angular rate limit based on speed
|
||||
self.apply_angle_last = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
|
||||
lat_active, CarControllerParams, self.VM)
|
||||
@@ -57,9 +91,34 @@ class CarController(CarControllerBase):
|
||||
self.apply_angle_command_last, lat_active = self.coop_steer.update(
|
||||
self.apply_angle_last, lat_active, self.coop_enabled, CS, self.VM,
|
||||
)
|
||||
can_sends.append(self.tesla_can.create_steering_control(self.apply_angle_command_last, lat_active))
|
||||
|
||||
if self.frame % 10 == 0:
|
||||
if self.coop_enabled and lat_active:
|
||||
model_limit_error_deg = abs(requested_angle - self.apply_angle_last)
|
||||
resume_limit_error_deg = self.coop_steer.resume_limit_error_deg
|
||||
cooperative_limit_error_deg = self.coop_steer.cooperative_limit_error_deg
|
||||
cooperative_offset_deg = self.coop_steer.cooperative_offset_deg
|
||||
combined_limit_error_deg = abs(requested_angle + cooperative_offset_deg - self.apply_angle_command_last)
|
||||
limit_values = (model_limit_error_deg, resume_limit_error_deg, cooperative_limit_error_deg,
|
||||
cooperative_offset_deg, combined_limit_error_deg)
|
||||
|
||||
if all(np.isfinite(value) for value in limit_values):
|
||||
self.steering_limit_info_valid = True
|
||||
self.model_limit_error_deg = model_limit_error_deg
|
||||
self.resume_limit_error_deg = resume_limit_error_deg
|
||||
self.cooperative_limit_error_deg = cooperative_limit_error_deg
|
||||
self.cooperative_offset_deg = cooperative_offset_deg
|
||||
self.steering_limit_mono_time = now_nanos
|
||||
self.combined_limit_error_deg = combined_limit_error_deg
|
||||
else:
|
||||
self._clear_steering_limit_info()
|
||||
|
||||
if self.CP.carFingerprint in LEGACY_CARS:
|
||||
cntr = (self.frame // 2) % 16
|
||||
can_sends.append(self.tesla_can.create_steering_control(cntr, self.apply_angle_command_last, lat_active))
|
||||
else:
|
||||
can_sends.append(self.tesla_can.create_steering_control(self.apply_angle_command_last, lat_active))
|
||||
|
||||
if self.frame % 10 == 0 and self.CP.carFingerprint not in LEGACY_CARS:
|
||||
can_sends.append(self.tesla_can.create_steering_allowed())
|
||||
|
||||
# Longitudinal control
|
||||
@@ -68,13 +127,21 @@ class CarController(CarControllerBase):
|
||||
state = 13 if CC.cruiseControl.cancel else 4 # 4=ACC_ON, 13=ACC_CANCEL_GENERIC_SILENT
|
||||
accel = float(np.clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
|
||||
cntr = (self.frame // 4) % 8
|
||||
can_sends.append(self.tesla_can.create_longitudinal_command(state, accel, cntr, self.frame, CS.out.vEgo, CS.out.gasPressed))
|
||||
if self.CP.carFingerprint in LEGACY_CARS:
|
||||
hw1_accel = accel if CC.longActive and not CC.cruiseControl.cancel else 0.
|
||||
hw1_active = CC.longActive and not CC.cruiseControl.cancel
|
||||
can_sends.append(self.tesla_can.create_longitudinal_command(state, hw1_accel, cntr, CS.out.vEgo, hw1_active, CS.out.gasPressed))
|
||||
else:
|
||||
can_sends.append(self.tesla_can.create_longitudinal_command(state, accel, cntr, self.frame, CS.out.vEgo, CS.out.gasPressed))
|
||||
|
||||
else:
|
||||
# Increment counter so cancel is prioritized even without openpilot longitudinal
|
||||
if CC.cruiseControl.cancel:
|
||||
cntr = (CS.das_control["DAS_controlCounter"] + 1) % 8
|
||||
can_sends.append(self.tesla_can.create_longitudinal_command(13, 0, cntr, self.frame, CS.out.vEgo, False))
|
||||
if self.CP.carFingerprint in LEGACY_CARS:
|
||||
can_sends.append(self.tesla_can.create_longitudinal_command(13, 0, cntr, CS.out.vEgo, False, CS.out.gasPressed))
|
||||
else:
|
||||
can_sends.append(self.tesla_can.create_longitudinal_command(13, 0, cntr, self.frame, CS.out.vEgo, False))
|
||||
|
||||
# TODO: HUD control
|
||||
new_actuators = actuators.as_builder()
|
||||
@@ -86,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
|
||||
@@ -102,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
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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
|
||||
from opendbc.car.tesla.values import (
|
||||
DBC, CANBUS, GEAR_MAP, STEER_DISENGAGE_THRESHOLD, STEER_THRESHOLD, TeslaSafetyFlags,
|
||||
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
|
||||
from opendbc.car.tesla.preap.nap_conf import nap_conf
|
||||
@@ -16,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
|
||||
@@ -25,8 +42,19 @@ class CarState(CarStateBase):
|
||||
def __init__(self, CP, FPCP):
|
||||
super().__init__(CP, FPCP)
|
||||
self.can_define = CANDefine(DBC[CP.carFingerprint][Bus.party])
|
||||
self.shifter_values = self.can_define.dv["DI_systemStatus"]["DI_gear"] if CP.carFingerprint != CAR.TESLA_MODEL_S_PREAP else \
|
||||
self.can_define.dv["DI_torque2"]["DI_gear"]
|
||||
if CP.carFingerprint in LEGACY_CARS:
|
||||
self.can_define_party = CANDefine(DBC[CP.carFingerprint][Bus.party])
|
||||
self.can_define_pt = CANDefine(DBC[CP.carFingerprint][Bus.pt])
|
||||
self.can_define_chassis = CANDefine(DBC[CP.carFingerprint][Bus.chassis])
|
||||
self.can_defines = {
|
||||
**self.can_define_party.dv,
|
||||
**self.can_define_pt.dv,
|
||||
**self.can_define_chassis.dv,
|
||||
}
|
||||
self.shifter_values = self.can_defines["DI_torque2"]["DI_gear"]
|
||||
else:
|
||||
self.shifter_values = self.can_define.dv["DI_systemStatus"]["DI_gear"] if CP.carFingerprint != CAR.TESLA_MODEL_S_PREAP else \
|
||||
self.can_define.dv["DI_torque2"]["DI_gear"]
|
||||
|
||||
self.autopark = False
|
||||
self.autopark_prev = False
|
||||
@@ -37,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(
|
||||
@@ -72,9 +102,25 @@ 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)
|
||||
if self.CP.carFingerprint in LEGACY_CARS:
|
||||
return self.update_legacy(can_parsers)
|
||||
|
||||
cp_party = can_parsers[Bus.party]
|
||||
cp_ap_party = can_parsers[Bus.ap_party]
|
||||
@@ -165,19 +211,109 @@ 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
|
||||
|
||||
def update_legacy(self, can_parsers):
|
||||
cp_party = can_parsers[Bus.party]
|
||||
cp_ap_party = can_parsers[Bus.ap_party]
|
||||
cp_pt = can_parsers[Bus.pt]
|
||||
cp_ap_pt = can_parsers[Bus.ap_pt]
|
||||
cp_chassis = can_parsers[Bus.chassis]
|
||||
ret = structs.CarState()
|
||||
fp_ret = custom.StarPilotCarState.new_message()
|
||||
|
||||
# Vehicle speed
|
||||
ret.vEgoRaw = cp_chassis.vl["ESP_B"]["ESP_vehicleSpeed"] * CV.KPH_TO_MS
|
||||
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
|
||||
|
||||
# Gas and brake
|
||||
ret.gasPressed = cp_pt.vl["DI_torque1"]["DI_pedalPos"] > 0
|
||||
ret.brake = 0
|
||||
ret.brakePressed = cp_chassis.vl["BrakeMessage"]["driverBrakeStatus"] != 1
|
||||
|
||||
# Steering wheel and EPAS status
|
||||
epas_status = cp_chassis.vl["EPAS_sysStatus"]
|
||||
self.hands_on_level = epas_status["EPAS_handsOnLevel"]
|
||||
ret.steeringAngleDeg = -epas_status["EPAS_internalSAS"]
|
||||
ret.steeringRateDeg = -cp_chassis.vl["STW_ANGLHP_STAT"]["StW_AnglHP_Spd"]
|
||||
ret.steeringTorque = -epas_status["EPAS_torsionBarTorque"]
|
||||
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > STEER_THRESHOLD, 5)
|
||||
|
||||
eac_status = self.can_defines["EPAS_sysStatus"]["EPAS_eacStatus"].get(int(epas_status["EPAS_eacStatus"]), None)
|
||||
ret.steerFaultPermanent = eac_status == "EAC_FAULT"
|
||||
ret.steerFaultTemporary = eac_status == "EAC_INHIBITED"
|
||||
eac_error_code = self.can_defines["EPAS_sysStatus"]["EPAS_eacErrorCode"].get(int(epas_status["EPAS_eacErrorCode"]), None)
|
||||
ret.steeringDisengage = self.hands_on_level >= 3 or (
|
||||
eac_status == "EAC_INHIBITED" and eac_error_code == "EAC_ERROR_HIGH_ANGLE_RATE_SAFETY"
|
||||
)
|
||||
|
||||
# Cruise
|
||||
cruise_state = self.can_defines["DI_state"]["DI_cruiseState"].get(int(cp_chassis.vl["DI_state"]["DI_cruiseState"]), None)
|
||||
speed_units = self.can_defines["DI_state"]["DI_speedUnits"].get(int(cp_chassis.vl["DI_state"]["DI_speedUnits"]), None)
|
||||
cruise_enabled = cruise_state in ("ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL")
|
||||
ret.cruiseState.enabled = cruise_enabled
|
||||
if speed_units == "KPH":
|
||||
ret.cruiseState.speed = max(cp_chassis.vl["DI_state"]["DI_hw1CruiseSet"] * CV.KPH_TO_MS, 1e-3)
|
||||
elif speed_units == "MPH":
|
||||
ret.cruiseState.speed = max(cp_chassis.vl["DI_state"]["DI_hw1CruiseSet"] * CV.MPH_TO_MS, 1e-3)
|
||||
ret.cruiseState.available = cruise_state == "STANDBY" or ret.cruiseState.enabled
|
||||
ret.cruiseState.standstill = False
|
||||
ret.standstill = ret.vEgoRaw < 0.1
|
||||
ret.accFaulted = cruise_state == "FAULT"
|
||||
|
||||
# Gear, body state, and safety state
|
||||
ret.gearShifter = GEAR_MAP[self.can_defines["DI_torque2"]["DI_gear"].get(
|
||||
int(cp_chassis.vl["DI_torque2"]["DI_gear"]), "DI_GEAR_INVALID")]
|
||||
|
||||
doors = ("DOOR_STATE_FL", "DOOR_STATE_FR", "DOOR_STATE_RL", "DOOR_STATE_RR", "DOOR_STATE_FrontTrunk", "BOOT_STATE")
|
||||
ret.doorOpen = any(
|
||||
self.can_defines["GTW_carState"][door].get(int(cp_chassis.vl["GTW_carState"][door]), "OPEN") == "OPEN"
|
||||
for door in doors
|
||||
)
|
||||
ret.leftBlinker = cp_chassis.vl["GTW_carState"]["BC_indicatorLStatus"] == 1
|
||||
ret.rightBlinker = cp_chassis.vl["GTW_carState"]["BC_indicatorRStatus"] == 1
|
||||
|
||||
_ = cp_chassis.vl["SDM1"]
|
||||
_ = cp_chassis.vl["RCM_status"]
|
||||
sd_time = cp_chassis.ts_nanos["SDM1"]["SDM_bcklDrivStatus"]
|
||||
rcm_time = cp_chassis.ts_nanos["RCM_status"]["RCM_buckleDriverStatus"]
|
||||
if sd_time and cp_chassis._last_update_nanos - sd_time <= 1_000_000_000:
|
||||
ret.seatbeltUnlatched = cp_chassis.vl["SDM1"]["SDM_bcklDrivStatus"] != 1
|
||||
elif rcm_time and cp_chassis._last_update_nanos - rcm_time <= 1_000_000_000:
|
||||
ret.seatbeltUnlatched = cp_chassis.vl["RCM_status"]["RCM_buckleDriverStatus"] != 1
|
||||
else:
|
||||
ret.seatbeltUnlatched = True
|
||||
|
||||
ret.stockAeb = cp_ap_pt.vl["DAS_control"]["DAS_aebEvent"] == 1
|
||||
ret.stockLkas = cp_ap_party.vl["DAS_steeringControl"]["DAS_steeringControlType"] == 2
|
||||
self.das_control = copy.copy(cp_ap_pt.vl["DAS_control"])
|
||||
return ret, fp_ret
|
||||
|
||||
@staticmethod
|
||||
def get_can_parsers(CP):
|
||||
if CP.carFingerprint == CAR.TESLA_MODEL_S_PREAP:
|
||||
return get_preap_can_parsers(CP)
|
||||
if CP.carFingerprint in LEGACY_CARS:
|
||||
return {
|
||||
Bus.party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.party),
|
||||
Bus.ap_party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.autopilot_party),
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CANBUS.party),
|
||||
Bus.ap_pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CANBUS.autopilot_party),
|
||||
Bus.chassis: CANParser(DBC[CP.carFingerprint][Bus.chassis], [("SDM1", 0), ("RCM_status", 0)], CANBUS.party),
|
||||
}
|
||||
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 {}),
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ class CooperativeSteeringController:
|
||||
self.angle_override = 0.0
|
||||
self.resume_rate_limiter_delta = SteerRateLimiter()
|
||||
self.resume_rate_limiter = SteerRateLimiter()
|
||||
self.resume_limit_error_deg = 0.0
|
||||
self.cooperative_limit_error_deg = 0.0
|
||||
self.cooperative_offset_deg = 0.0
|
||||
|
||||
def reset_override_state(self, apply_angle: float) -> None:
|
||||
self.apply_angle_last = apply_angle
|
||||
@@ -105,19 +108,25 @@ class CooperativeSteeringController:
|
||||
return self.resume_rate_limiter.update(apply_angle, angle_rate_delta)
|
||||
|
||||
def update(self, apply_angle: float, lat_active: bool, enabled: bool, CS, VM: VehicleModel) -> tuple[float, bool]:
|
||||
self.resume_limit_error_deg = 0.0
|
||||
self.cooperative_limit_error_deg = 0.0
|
||||
self.cooperative_offset_deg = 0.0
|
||||
if not enabled:
|
||||
self.reset_resume_state(apply_angle)
|
||||
self.reset_override_state(apply_angle)
|
||||
return apply_angle, lat_active
|
||||
|
||||
requested_angle = apply_angle
|
||||
apply_angle = self.apply_resume_rate_limit(lat_active, apply_angle)
|
||||
self.resume_limit_error_deg = abs(requested_angle - apply_angle)
|
||||
if not lat_active:
|
||||
self.reset_override_state(apply_angle)
|
||||
return apply_angle, False
|
||||
|
||||
apply_angle_delta = apply_angle - self.apply_angle_last
|
||||
self.apply_angle_last = apply_angle
|
||||
apply_angle += self.update_override_angle(apply_angle_delta, CS.out.steeringTorque, CS.out.vEgo, VM)
|
||||
self.cooperative_offset_deg = self.update_override_angle(apply_angle_delta, CS.out.steeringTorque, CS.out.vEgo, VM)
|
||||
apply_angle += self.cooperative_offset_deg
|
||||
|
||||
limited_angle = apply_steer_angle_limits_vm(
|
||||
apply_angle,
|
||||
@@ -129,5 +138,6 @@ class CooperativeSteeringController:
|
||||
VM,
|
||||
)
|
||||
self.coop_apply_angle_last = limited_angle
|
||||
self.cooperative_limit_error_deg = abs(apply_angle - limited_angle)
|
||||
self.unwind_override_angle(apply_angle - limited_angle)
|
||||
return limited_angle, True
|
||||
|
||||
@@ -5,6 +5,12 @@ from opendbc.car.tesla.values import CAR
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
FW_VERSIONS = {
|
||||
CAR.TESLA_MODEL_S_HW1: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'1016704-00-HAA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'\x10\x00A',
|
||||
],
|
||||
},
|
||||
CAR.TESLA_MODEL_3: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'TeM3_E014p10_0.0.0 (16),E014.17.00',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from opendbc.car import get_safety_config, structs
|
||||
from opendbc.car import Bus, get_safety_config, structs
|
||||
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
|
||||
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
|
||||
@@ -32,8 +38,26 @@ class CarInterface(CarInterfaceBase):
|
||||
if candidate == CAR.TESLA_MODEL_S_PREAP:
|
||||
return get_preap_params(ret)
|
||||
|
||||
if candidate in LEGACY_CARS:
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla, TeslaSafetyFlags.FLAG_HW1.value)]
|
||||
ret.steerLimitTimer = 0.4
|
||||
ret.steerActuatorDelay = 0.1
|
||||
ret.steerAtStandstill = True
|
||||
ret.steerControlType = structs.CarParams.SteerControlType.angle
|
||||
ret.radarUnavailable = Bus.radar not in DBC[candidate]
|
||||
ret.radarTimeStepDEPRECATED = 0.125
|
||||
ret.alphaLongitudinalAvailable = True
|
||||
|
||||
if alpha_long:
|
||||
ret.openpilotLongitudinalControl = True
|
||||
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LONG_CONTROL.value
|
||||
return ret
|
||||
|
||||
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
|
||||
@@ -16,7 +16,7 @@ class RadarInterface(RadarInterfaceBase):
|
||||
def __init__(self, CP):
|
||||
super().__init__(CP)
|
||||
|
||||
self.radar_off_can = CP.radarUnavailable or CP.carFingerprint != CAR.TESLA_MODEL_S_PREAP
|
||||
self.radar_off_can = CP.radarUnavailable or Bus.radar not in DBC[CP.carFingerprint]
|
||||
self.updated_messages: set[int] = set()
|
||||
self.track_id = 0
|
||||
self.radar_offset = float(nap_conf.radar_offset) if CP.carFingerprint == CAR.TESLA_MODEL_S_PREAP else 0.0
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import V_CRUISE_MAX
|
||||
from opendbc.car.tesla.values import CANBUS, CarControllerParams
|
||||
|
||||
|
||||
class TeslaCANRaven:
|
||||
"""CAN commands used by the legacy Model S/X powertrain and EPAS buses."""
|
||||
|
||||
def __init__(self, packers):
|
||||
self.packers = packers
|
||||
self.CCP = CarControllerParams
|
||||
self.jerk_upper = self.CCP.JERK_LIMIT_MAX
|
||||
self.jerk_lower = self.CCP.JERK_LIMIT_MIN
|
||||
|
||||
@staticmethod
|
||||
def checksum(msg_id, dat):
|
||||
return ((msg_id & 0xFF) + ((msg_id >> 8) & 0xFF) + sum(dat)) & 0xFF
|
||||
|
||||
def create_steering_control(self, counter, angle, enabled):
|
||||
values = {
|
||||
"DAS_steeringControlCounter": counter,
|
||||
"DAS_steeringAngleRequest": -angle,
|
||||
"DAS_steeringHapticRequest": 0,
|
||||
"DAS_steeringControlType": 1 if enabled else 0,
|
||||
}
|
||||
data = self.packers[CANBUS.party].make_can_msg("DAS_steeringControl", CANBUS.party, values)[1]
|
||||
values["DAS_steeringControlChecksum"] = self.checksum(0x488, data[:3])
|
||||
return self.packers[CANBUS.party].make_can_msg("DAS_steeringControl", CANBUS.party, values)
|
||||
|
||||
def create_longitudinal_command(self, acc_state, accel, counter, v_ego, active, gas_pressed):
|
||||
set_speed = max(v_ego * CV.MS_TO_KPH, 0)
|
||||
if active:
|
||||
set_speed = 0 if accel < 0 else V_CRUISE_MAX
|
||||
|
||||
if gas_pressed:
|
||||
self.jerk_upper = self.jerk_lower = 0.0
|
||||
else:
|
||||
self.jerk_lower = max(self.jerk_lower - self.CCP.JERK_RAMP_RATE, self.CCP.JERK_LIMIT_MIN)
|
||||
self.jerk_upper = min(self.jerk_upper + self.CCP.JERK_RAMP_RATE, self.CCP.JERK_LIMIT_MAX)
|
||||
|
||||
values = {
|
||||
"DAS_setSpeed": set_speed,
|
||||
"DAS_accState": acc_state,
|
||||
"DAS_aebEvent": 0,
|
||||
"DAS_jerkMin": self.jerk_lower,
|
||||
"DAS_jerkMax": self.jerk_upper,
|
||||
"DAS_accelMin": accel,
|
||||
"DAS_accelMax": max(accel, 0),
|
||||
"DAS_controlCounter": counter,
|
||||
}
|
||||
data = self.packers[CANBUS.party].make_can_msg("DAS_control", CANBUS.party, values)[1]
|
||||
values["DAS_controlChecksum"] = self.checksum(0x2b9, data[:7])
|
||||
return self.packers[CANBUS.party].make_can_msg("DAS_control", CANBUS.party, values)
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline AP1/HW1 CAN, radar, controller, and panda-safety replay.
|
||||
|
||||
Usage: PYTHONPATH=. python -m opendbc.car.tesla.tests.replay_hw1_route PATH_TO_RLOGS
|
||||
|
||||
The supplied Pre-AP recording contains stock AP commands copied to bus 0 while
|
||||
ELM327/old firmware forwarded traffic. The counterfactual run skips those copies:
|
||||
the HW1 safety mode blocks stock 0x488/0x2b9 forwarding on bus 2. This is NOT a
|
||||
physical HW1 drive; it cannot validate engagement or steering actuation on-car.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.tesla.carcontroller import CarController
|
||||
from opendbc.car.tesla.carstate import CarState
|
||||
from opendbc.car.tesla.interface import CarInterface
|
||||
from opendbc.car.tesla.radar_interface import RadarInterface
|
||||
from opendbc.car.tesla.values import CAR, DBC, TeslaSafetyFlags
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def replay(paths: list[Path], simulate_active: bool = False):
|
||||
fp = {0: {0x201: 5}, 1: {}, 2: {}}
|
||||
cp = CarInterface.get_params(CAR.TESLA_MODEL_S_HW1, fp, [], True, False, False, None)
|
||||
assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.tesla
|
||||
assert cp.safetyConfigs[0].safetyParam == TeslaSafetyFlags.FLAG_HW1.value | TeslaSafetyFlags.LONG_CONTROL.value
|
||||
safety = libsafety_py.libsafety
|
||||
assert safety.set_safety_hooks(int(structs.CarParams.SafetyModel.tesla), cp.safetyConfigs[0].safetyParam) == 0
|
||||
safety.init_tests()
|
||||
|
||||
parsers = CarState.get_can_parsers(cp)
|
||||
cs = CarState(cp, custom.StarPilotCarParams.new_message())
|
||||
controller = CarController(DBC[CAR.TESLA_MODEL_S_HW1], cp)
|
||||
active_controller = CarController(DBC[CAR.TESLA_MODEL_S_HW1], cp) if simulate_active else None
|
||||
radar = RadarInterface(cp)
|
||||
stats = Counter()
|
||||
first_rejected = []
|
||||
active_rejected = []
|
||||
last_ap_command: dict[tuple[int, bytes], int] = {}
|
||||
suppressed_examples = []
|
||||
|
||||
for path in paths:
|
||||
for event in LogReader(str(path)):
|
||||
if event.which() != "can":
|
||||
continue
|
||||
t = event.logMonoTime
|
||||
frames = [(x.address, bytes(x.dat), x.src) for x in event.can]
|
||||
stock_in_event = {(a, d) for a, d, b in frames if b == 2 and a in (0x488, 0x2b9)}
|
||||
for a, d, b in frames:
|
||||
if b == 2 and a in (0x488, 0x2b9):
|
||||
last_ap_command[(a, d)] = t
|
||||
if b == 0 and a in (0x488, 0x2b9):
|
||||
seen = last_ap_command.get((a, d), -1)
|
||||
if (a, d) in stock_in_event or (0 <= t - seen < 250_000_000):
|
||||
stats["suppressed_bus0_stock_copies"] += 1
|
||||
continue
|
||||
stats["unmatched_bus0_stock_commands"] += 1
|
||||
if len(suppressed_examples) < 5:
|
||||
suppressed_examples.append((path.name, t, hex(a), d.hex()))
|
||||
if b < 128:
|
||||
stats["physical_rx"] += 1
|
||||
if not safety.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d)):
|
||||
stats["rx_rejected"] += 1
|
||||
if b == 2 and a in (0x488, 0x2b9):
|
||||
stats["stock_forward_blocked"] += safety.safety_fwd_hook(b, a) == -1
|
||||
|
||||
safety.set_timer((t // 1000) & 0xffffffff)
|
||||
safety.safety_tick_current_safety_config()
|
||||
stats["safety_invalid_ticks"] += not safety.safety_config_valid()
|
||||
stats["relay_malfunction_ticks"] += safety.get_relay_malfunction()
|
||||
stats["controls_allowed_ticks"] += safety.get_controls_allowed()
|
||||
|
||||
batch = [(t, frames)]
|
||||
for parser in parsers.values():
|
||||
parser.update(batch)
|
||||
stats["invalid_car_parser_ticks"] += not parser.can_valid
|
||||
out, _ = cs.update(parsers, None)
|
||||
cs.out = out
|
||||
stats["carstate_faulted_ticks"] += out.accFaulted
|
||||
stats["seatbelt_unlatched_ticks"] += out.seatbeltUnlatched
|
||||
stats["steering_inhibited_ticks"] += out.steerFaultTemporary
|
||||
stats["cruise_engaged_ticks"] += out.cruiseState.enabled
|
||||
|
||||
radar_data = radar.update(batch)
|
||||
if radar_data is not None:
|
||||
stats["radar_updates"] += 1
|
||||
stats["radar_points"] += len(radar_data.points)
|
||||
stats["radar_error_updates"] += radar_data.errors.canError or radar_data.errors.radarFault
|
||||
|
||||
cc = structs.CarControl.new_message()
|
||||
cc.actuators.steeringAngleDeg = out.steeringAngleDeg
|
||||
cc.actuators.accel = 0.
|
||||
# Do not fabricate engagement on the actual faulted/standby route.
|
||||
_, sends = controller.update(cc.as_reader(), cs, t, None)
|
||||
for a, d, b in sends:
|
||||
stats["generated_tx"] += 1
|
||||
stats[f"generated_{hex(a)}"] += 1
|
||||
if not safety.safety_tx_hook(libsafety_py.make_CANPacket(a, b, d)):
|
||||
stats["tx_rejected"] += 1
|
||||
if len(first_rejected) < 5:
|
||||
first_rejected.append((path.name, t, hex(a), d.hex(), out.steeringAngleDeg, safety.get_relay_malfunction()))
|
||||
|
||||
if active_controller is not None:
|
||||
# A synthetic gate test only. This recording never engaged cruise, so
|
||||
# enabling controls here does NOT represent an actual car-state transition.
|
||||
eligible = (not (out.steerFaultTemporary or out.steerFaultPermanent or out.steeringDisengage or out.accFaulted or
|
||||
out.gasPressed or out.brakePressed or out.stockAeb or out.stockLkas) and out.vEgoRaw > 2.)
|
||||
simulated = structs.CarControl.new_message()
|
||||
simulated.latActive = eligible
|
||||
simulated.longActive = eligible
|
||||
simulated.actuators.steeringAngleDeg = out.steeringAngleDeg
|
||||
simulated.actuators.accel = 0.5 if eligible else 0.
|
||||
_, active_sends = active_controller.update(simulated.as_reader(), cs, t, None)
|
||||
if eligible:
|
||||
stats["simulated_eligible_ticks"] += 1
|
||||
safety.set_controls_allowed(True)
|
||||
for a, d, b in active_sends:
|
||||
stats["simulated_tx"] += 1
|
||||
stats[f"simulated_{hex(a)}"] += 1
|
||||
if not safety.safety_tx_hook(libsafety_py.make_CANPacket(a, b, d)):
|
||||
stats["simulated_tx_rejected"] += 1
|
||||
if len(active_rejected) < 5:
|
||||
active_rejected.append((path.name, t, hex(a), d.hex(), out.steeringAngleDeg, out.vEgoRaw))
|
||||
safety.set_controls_allowed(False)
|
||||
stats["can_events"] += 1
|
||||
print(f"{path.name}: {dict(stats)}", flush=True)
|
||||
|
||||
print(f"unmatched bus-0 command examples: {suppressed_examples}")
|
||||
print(f"rejected TX examples: {first_rejected}")
|
||||
print(f"rejected synthetic-active TX examples: {active_rejected}")
|
||||
print(f"final: {dict(stats)}")
|
||||
return stats
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
argp = argparse.ArgumentParser(description=__doc__)
|
||||
argp.add_argument("rlogs", type=Path, help="directory containing segment rlog.zst files")
|
||||
argp.add_argument("--simulate-active", action="store_true", help="force safety engagement only on healthy standby samples")
|
||||
args = argp.parse_args()
|
||||
files = sorted(args.rlogs.glob("*.rlog.zst"))
|
||||
if not files:
|
||||
argp.error("no *.rlog.zst files found")
|
||||
replay(files, args.simulate_active)
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -73,3 +74,110 @@ def test_safety_flag_is_model_3_only(candidate, enabled, expected):
|
||||
|
||||
if candidate != CAR.TESLA_MODEL_S_PREAP:
|
||||
assert CarController(DBC[candidate], params).coop_enabled is expected
|
||||
|
||||
|
||||
def assert_finite_nonnegative_limit_errors(controller):
|
||||
errors = (controller.resume_limit_error_deg, controller.cooperative_limit_error_deg)
|
||||
assert all(math.isfinite(error) and error >= 0.0 for error in errors)
|
||||
assert math.isfinite(controller.cooperative_offset_deg)
|
||||
|
||||
|
||||
def test_zero_torque_has_zero_cooperative_diagnostics(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
|
||||
angle, lat_active = controller.update(0.0, True, True, make_car_state(), vehicle_model)
|
||||
|
||||
assert angle == 0.0
|
||||
assert lat_active
|
||||
assert controller.resume_limit_error_deg == 0.0
|
||||
assert controller.cooperative_limit_error_deg == 0.0
|
||||
assert controller.cooperative_offset_deg == 0.0
|
||||
assert_finite_nonnegative_limit_errors(controller)
|
||||
|
||||
|
||||
def test_steady_light_torque_reports_offset_without_real_limiting(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
|
||||
for _ in range(100):
|
||||
controller.update(0.0, True, True, make_car_state(torque=0.9), vehicle_model)
|
||||
|
||||
assert controller.cooperative_offset_deg > 2.5
|
||||
assert controller.resume_limit_error_deg < 2.5
|
||||
assert controller.cooperative_limit_error_deg < 2.5
|
||||
assert_finite_nonnegative_limit_errors(controller)
|
||||
|
||||
|
||||
def test_torque_reversal_updates_signed_offset_without_negative_errors(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
for _ in range(100):
|
||||
controller.update(0.0, True, True, make_car_state(torque=0.9), vehicle_model)
|
||||
|
||||
for _ in range(200):
|
||||
controller.update(0.0, True, True, make_car_state(torque=-0.9), vehicle_model)
|
||||
|
||||
assert controller.cooperative_offset_deg < -2.5
|
||||
assert_finite_nonnegative_limit_errors(controller)
|
||||
|
||||
|
||||
def test_release_reports_gradual_offset_unwind(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
for _ in range(100):
|
||||
controller.update(0.0, True, True, make_car_state(torque=1.5), vehicle_model)
|
||||
|
||||
offsets = []
|
||||
for _ in range(100):
|
||||
controller.update(0.0, True, True, make_car_state(), vehicle_model)
|
||||
offsets.append(controller.cooperative_offset_deg)
|
||||
|
||||
assert offsets[0] > offsets[-1] >= 0.0
|
||||
assert all(next_offset <= offset for offset, next_offset in zip(offsets, offsets[1:]))
|
||||
assert offsets[-1] == pytest.approx(0.0, abs=1e-6)
|
||||
assert_finite_nonnegative_limit_errors(controller)
|
||||
|
||||
|
||||
def test_resume_ramp_reports_resume_limiting(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
controller.reset_resume_state(0.0)
|
||||
controller.reset_override_state(0.0)
|
||||
|
||||
controller.update(20.0, True, True, make_car_state(), vehicle_model)
|
||||
|
||||
assert controller.resume_limit_error_deg > 2.5
|
||||
assert controller.cooperative_limit_error_deg < 2.5
|
||||
assert controller.cooperative_offset_deg == 0.0
|
||||
|
||||
|
||||
def test_final_limiter_reports_cooperative_target_clipping(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
controller.reset_resume_state(20.0)
|
||||
controller.reset_override_state(0.0)
|
||||
|
||||
controller.update(20.0, True, True, make_car_state(), vehicle_model)
|
||||
|
||||
assert controller.resume_limit_error_deg == 0.0
|
||||
assert controller.cooperative_limit_error_deg > 2.5
|
||||
assert controller.cooperative_offset_deg == 0.0
|
||||
|
||||
|
||||
def test_final_limiter_remains_visible_with_light_torque(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
controller.reset_resume_state(20.0)
|
||||
controller.reset_override_state(0.0)
|
||||
|
||||
controller.update(20.0, True, True, make_car_state(torque=-0.9), vehicle_model)
|
||||
|
||||
assert abs(controller.cooperative_offset_deg) > 0.0
|
||||
assert controller.cooperative_limit_error_deg > 2.5
|
||||
|
||||
|
||||
def test_diagnostics_reset_on_disabled_update(vehicle_model):
|
||||
controller = CooperativeSteeringController()
|
||||
controller.reset_resume_state(20.0)
|
||||
controller.update(20.0, True, True, make_car_state(), vehicle_model)
|
||||
assert controller.cooperative_limit_error_deg > 2.5
|
||||
|
||||
controller.update(4.0, True, False, make_car_state(torque=2.0), vehicle_model)
|
||||
|
||||
assert controller.resume_limit_error_deg == 0.0
|
||||
assert controller.cooperative_limit_error_deg == 0.0
|
||||
assert controller.cooperative_offset_deg == 0.0
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import cereal.messaging as messaging
|
||||
|
||||
from cereal import car
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.tesla.carcontroller import CarController
|
||||
from opendbc.car.tesla.interface import CarInterface
|
||||
from opendbc.car.tesla.values import CAR, DBC
|
||||
|
||||
|
||||
BASELINE_SHA = "a80064be4fdf4b8765a5e9dec44d8a5c266f48a8"
|
||||
BASELINE_SOURCE_SHA256 = {
|
||||
"opendbc_repo/opendbc/car/tesla/coop_steering.py": "9c9d60bbfae2aaa0d8c1fca203a9fa19a14a19feef5aa89e85ecaa6f6502a9f0",
|
||||
"opendbc_repo/opendbc/car/tesla/carcontroller.py": "1ef3cf646bc4b3c398bd12010e624b90e083426152d632fafb5b2e93fd3d8661",
|
||||
}
|
||||
BASELINE_FIXTURE = Path(__file__).parent / "fixtures" / "coop_steering_baseline_a80064be.json"
|
||||
|
||||
|
||||
def make_params(candidate=CAR.TESLA_MODEL_3, cooperative=True):
|
||||
toggles = SimpleNamespace(tesla_cooperative_steering=cooperative, trailer_load_kg=0.0)
|
||||
return CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False, toggles)
|
||||
|
||||
|
||||
def make_car_state(torque=0.0, speed=15.0, angle=0.0, steering_disengage=False):
|
||||
return SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
steeringTorque=torque,
|
||||
steeringAngleDeg=angle,
|
||||
steeringDisengage=steering_disengage,
|
||||
vEgo=speed,
|
||||
vEgoRaw=speed,
|
||||
gasPressed=False,
|
||||
),
|
||||
hands_on_level=0,
|
||||
das_control={"DAS_controlCounter": 0},
|
||||
)
|
||||
|
||||
|
||||
def make_control(requested_angle=0.0, lat_active=True):
|
||||
control = car.CarControl.new_message()
|
||||
control.latActive = lat_active
|
||||
control.actuators.steeringAngleDeg = requested_angle
|
||||
return control.as_reader()
|
||||
|
||||
|
||||
def make_controller(candidate=CAR.TESLA_MODEL_3, cooperative=True):
|
||||
params = make_params(candidate, cooperative)
|
||||
return CarController(DBC[candidate], params)
|
||||
|
||||
|
||||
def run_frame(controller, requested_angle=0.0, torque=0.0, speed=15.0, measured_angle=0.0,
|
||||
lat_active=True, steering_disengage=False, now_nanos=1_000_000_000):
|
||||
return controller.update(
|
||||
make_control(requested_angle, lat_active),
|
||||
make_car_state(torque, speed, measured_angle, steering_disengage),
|
||||
now_nanos,
|
||||
SimpleNamespace(),
|
||||
)
|
||||
|
||||
|
||||
def get_limit_info(controller):
|
||||
return SimpleNamespace(**controller.get_steering_limit_info())
|
||||
|
||||
|
||||
def legacy_actuator_dict(actuators):
|
||||
return actuators.to_dict()
|
||||
|
||||
|
||||
def test_steering_limit_info_defaults_to_invalid():
|
||||
controller = make_controller()
|
||||
|
||||
info = get_limit_info(controller)
|
||||
assert not info.valid
|
||||
assert info.monoTime == 0
|
||||
|
||||
|
||||
def test_steering_limit_info_round_trips_through_custom_message():
|
||||
message = messaging.new_message("starpilotCarControl", valid=True)
|
||||
info = message.starpilotCarControl.steeringLimitInfo
|
||||
info.valid = True
|
||||
info.modelLimitErrorDeg = 1.25
|
||||
info.resumeLimitErrorDeg = 0.5
|
||||
info.cooperativeLimitErrorDeg = 2.0
|
||||
info.cooperativeOffsetDeg = -4.5
|
||||
info.monoTime = 1_234_567_890
|
||||
info.combinedLimitErrorDeg = 3.75
|
||||
|
||||
restored = messaging.log_from_bytes(message.to_bytes())
|
||||
restored_info = restored.starpilotCarControl.steeringLimitInfo
|
||||
assert restored_info.valid
|
||||
assert restored_info.modelLimitErrorDeg == 1.25
|
||||
assert restored_info.resumeLimitErrorDeg == 0.5
|
||||
assert restored_info.cooperativeLimitErrorDeg == 2.0
|
||||
assert restored_info.cooperativeOffsetDeg == -4.5
|
||||
assert restored_info.monoTime == 1_234_567_890
|
||||
assert restored_info.combinedLimitErrorDeg == 3.75
|
||||
|
||||
|
||||
def test_active_cooperative_controller_reports_diagnostics():
|
||||
controller = make_controller()
|
||||
requested_angle = 20.0
|
||||
now_nanos = 1_234_567_890
|
||||
|
||||
actuators, _ = run_frame(controller, requested_angle, torque=0.9, measured_angle=0.0, now_nanos=now_nanos)
|
||||
|
||||
info = get_limit_info(controller)
|
||||
assert info.valid
|
||||
assert info.monoTime == now_nanos
|
||||
assert info.modelLimitErrorDeg == pytest.approx(abs(requested_angle - controller.apply_angle_last), abs=1e-5)
|
||||
assert info.resumeLimitErrorDeg == pytest.approx(controller.coop_steer.resume_limit_error_deg, abs=1e-5)
|
||||
assert info.cooperativeLimitErrorDeg == pytest.approx(controller.coop_steer.cooperative_limit_error_deg, abs=1e-5)
|
||||
assert info.cooperativeOffsetDeg == pytest.approx(controller.coop_steer.cooperative_offset_deg, abs=1e-5)
|
||||
assert info.combinedLimitErrorDeg == pytest.approx(
|
||||
abs(requested_angle + info.cooperativeOffsetDeg - actuators.steeringAngleDeg), abs=1e-5,
|
||||
)
|
||||
assert info.modelLimitErrorDeg > 2.5
|
||||
assert info.cooperativeOffsetDeg > 0.0
|
||||
assert info.combinedLimitErrorDeg > 2.5
|
||||
errors = (info.modelLimitErrorDeg, info.resumeLimitErrorDeg,
|
||||
info.cooperativeLimitErrorDeg, info.combinedLimitErrorDeg)
|
||||
assert all(math.isfinite(error) and error >= 0.0 for error in errors)
|
||||
|
||||
|
||||
def test_cooperative_offset_alone_does_not_become_limiter_error():
|
||||
controller = make_controller()
|
||||
actuators = None
|
||||
|
||||
for frame in range(200):
|
||||
actuators, _ = run_frame(controller, torque=0.9, now_nanos=1_000_000_000 + frame * 10_000_000)
|
||||
|
||||
assert actuators is not None
|
||||
info = get_limit_info(controller)
|
||||
assert info.valid
|
||||
assert info.cooperativeOffsetDeg > 2.5
|
||||
assert info.modelLimitErrorDeg < 2.5
|
||||
assert info.resumeLimitErrorDeg < 2.5
|
||||
assert info.cooperativeLimitErrorDeg < 2.5
|
||||
assert info.combinedLimitErrorDeg < 2.5
|
||||
|
||||
|
||||
def test_combined_error_keeps_two_same_direction_small_limits_visible():
|
||||
controller = make_controller()
|
||||
# Prime the resume limiter to the first-stage output for this literal input.
|
||||
controller.coop_steer.reset_resume_state(-0.9954867959022522)
|
||||
|
||||
actuators, _ = run_frame(controller, -2.5, torque=-1.5, speed=12.5, measured_angle=0.0)
|
||||
|
||||
info = get_limit_info(controller)
|
||||
assert info.modelLimitErrorDeg == pytest.approx(1.5045133, abs=1e-5)
|
||||
assert info.resumeLimitErrorDeg == pytest.approx(0.0, abs=1e-5)
|
||||
assert info.cooperativeLimitErrorDeg == pytest.approx(1.5045133, abs=1e-5)
|
||||
assert info.modelLimitErrorDeg < 2.5
|
||||
assert info.cooperativeLimitErrorDeg < 2.5
|
||||
assert info.combinedLimitErrorDeg == pytest.approx(3.0090265, abs=1e-5)
|
||||
assert info.combinedLimitErrorDeg > 2.5
|
||||
|
||||
|
||||
def test_intervening_100hz_frame_retains_matching_50hz_sample():
|
||||
controller = make_controller()
|
||||
first, _ = run_frame(controller, 8.0, torque=0.9, now_nanos=1_000_000_000)
|
||||
first_info = controller.get_steering_limit_info()
|
||||
|
||||
second, _ = run_frame(controller, -40.0, torque=-1.5, now_nanos=1_010_000_000)
|
||||
|
||||
assert controller.get_steering_limit_info() == first_info
|
||||
assert controller.get_steering_limit_info()["monoTime"] == 1_000_000_000
|
||||
|
||||
|
||||
def test_inactive_interval_clears_sample_until_next_steering_update():
|
||||
controller = make_controller()
|
||||
active, _ = run_frame(controller, 8.0, torque=0.9, now_nanos=1_000_000_000)
|
||||
assert get_limit_info(controller).valid
|
||||
|
||||
inactive, _ = run_frame(controller, 8.0, torque=0.9, lat_active=False, now_nanos=1_010_000_000)
|
||||
assert not get_limit_info(controller).valid
|
||||
assert get_limit_info(controller).monoTime == 0
|
||||
|
||||
resumed, _ = run_frame(controller, 8.0, torque=0.9, now_nanos=1_020_000_000)
|
||||
assert get_limit_info(controller).valid
|
||||
assert get_limit_info(controller).monoTime == 1_020_000_000
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("candidate", "cooperative", "steering_disengage"), (
|
||||
(CAR.TESLA_MODEL_3, False, False),
|
||||
(CAR.TESLA_MODEL_Y, True, False),
|
||||
(CAR.TESLA_MODEL_3, True, True),
|
||||
))
|
||||
def test_diagnostics_invalid_when_not_in_supported_active_path(candidate, cooperative, steering_disengage):
|
||||
controller = make_controller(candidate, cooperative)
|
||||
|
||||
actuators, _ = run_frame(controller, torque=1.5, steering_disengage=steering_disengage)
|
||||
|
||||
info = get_limit_info(controller)
|
||||
assert not info.valid
|
||||
assert info.monoTime == 0
|
||||
|
||||
|
||||
def test_actual_actuators_and_steering_can_match_pinned_baseline_fixture():
|
||||
fixture = json.loads(BASELINE_FIXTURE.read_text())
|
||||
assert fixture["metadata"] == {
|
||||
"schemaVersion": 1,
|
||||
"baselineSha": BASELINE_SHA,
|
||||
"baselineSourceSha256": BASELINE_SOURCE_SHA256,
|
||||
"frameCount": 386,
|
||||
}
|
||||
|
||||
candidate = make_controller()
|
||||
for expected in fixture["frames"]:
|
||||
inputs = expected["input"]
|
||||
candidate_actuators, candidate_can = run_frame(
|
||||
candidate,
|
||||
inputs["requestedAngleDeg"],
|
||||
inputs["torqueNm"],
|
||||
inputs["speedMps"],
|
||||
inputs["measuredAngleDeg"],
|
||||
inputs["latActive"],
|
||||
inputs["steeringDisengage"],
|
||||
inputs["nowNanos"],
|
||||
)
|
||||
|
||||
assert legacy_actuator_dict(candidate_actuators) == expected["actuators"]
|
||||
assert [[address, data.hex(), bus] for address, data, bus in candidate_can] == expected["can"]
|
||||
@@ -0,0 +1,134 @@
|
||||
import pytest
|
||||
|
||||
from cereal import custom
|
||||
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.fw_versions import match_fw_to_car
|
||||
from opendbc.car.tesla.carcontroller import CarController
|
||||
from opendbc.car.tesla.carstate import CarState
|
||||
from opendbc.car.tesla.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.tesla.interface import CarInterface
|
||||
from opendbc.car.tesla.teslacan_legacy import TeslaCANRaven
|
||||
from opendbc.car.tesla.values import CANBUS, CAR, DBC, TeslaSafetyFlags
|
||||
|
||||
|
||||
def test_hw1_is_distinct_from_preap_and_does_not_change_can_buses():
|
||||
preap = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP)
|
||||
hw1 = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW1)
|
||||
|
||||
assert preap.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.teslaPreAP
|
||||
assert hw1.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.tesla
|
||||
assert hw1.safetyConfigs[0].safetyParam == TeslaSafetyFlags.FLAG_HW1.value
|
||||
assert hw1.radarTimeStepDEPRECATED == pytest.approx(0.125)
|
||||
assert preap.radarTimeStepDEPRECATED == pytest.approx(0.05)
|
||||
assert CANBUS.party == 0 and CANBUS.autopilot_party == 2
|
||||
assert CarState.get_can_parsers(hw1)[Bus.ap_pt].bus == 2
|
||||
assert CarState.get_can_parsers(hw1)[Bus.chassis].message_states[0x211].ignore_alive
|
||||
assert CarState.get_can_parsers(preap)[Bus.party].bus == 0
|
||||
|
||||
|
||||
def test_hw1_requires_explicit_alpha_long_for_acceleration():
|
||||
ret = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW1)
|
||||
hw1 = CarInterface._get_params(ret, CAR.TESLA_MODEL_S_HW1, {0: {0x201: 5}}, [], True, False, False)
|
||||
assert hw1.openpilotLongitudinalControl
|
||||
assert hw1.safetyConfigs[0].safetyParam == TeslaSafetyFlags.FLAG_HW1.value | TeslaSafetyFlags.LONG_CONTROL.value
|
||||
|
||||
|
||||
def test_ap1_eps_fw_matches_hw1_without_matching_preap():
|
||||
version = FW_VERSIONS[CAR.TESLA_MODEL_S_HW1][(structs.CarParams.Ecu.eps, 0x730, None)][0]
|
||||
fw = structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.eps, address=0x730, brand="tesla", fwVersion=version)
|
||||
exact, candidates = match_fw_to_car([fw], "", log=False)
|
||||
assert exact and candidates == {CAR.TESLA_MODEL_S_HW1}
|
||||
|
||||
|
||||
def test_hw1_display_and_cruise_bytes_do_not_change_preap_signals():
|
||||
# Captured bus-0 DI_state (0x368) from the AP1 route: display 9 MPH, set speed 10 MPH.
|
||||
parser = CANParser("tesla_can", [(0x368, 0)], 0)
|
||||
parser.message_states[0x368].ignore_counter = True
|
||||
frames = [(0x368, bytes.fromhex("84185e3009980a2d"), 0)]
|
||||
parser.update([(1_000_000_000, frames)])
|
||||
parser.update([(2_000_000_000, frames)])
|
||||
state = parser.vl["DI_state"]
|
||||
assert state["DI_hw1DigitalSpeed"] == 9
|
||||
assert state["DI_hw1CruiseSet"] == 10
|
||||
assert state["DI_digitalSpeed"] == 10
|
||||
assert state["DI_cruiseSet"] != 10
|
||||
|
||||
|
||||
def test_hw1_packer_emits_bus_zero_with_matching_checksums():
|
||||
packer = CANPacker(DBC[CAR.TESLA_MODEL_S_HW1][Bus.party])
|
||||
tesla_can = TeslaCANRaven({CANBUS.party: packer})
|
||||
for msg, expected_addr, checksum_index in (
|
||||
(tesla_can.create_steering_control(0, 0, False), 0x488, 3),
|
||||
(tesla_can.create_longitudinal_command(13, 0, 0, 10, False, False), 0x2b9, 7),
|
||||
):
|
||||
addr, data, bus = msg
|
||||
assert addr == expected_addr and bus == 0
|
||||
assert data[checksum_index] == TeslaCANRaven.checksum(addr, data[:checksum_index])
|
||||
|
||||
|
||||
def test_hw1_cancel_clears_acceleration_and_does_not_request_max_speed():
|
||||
cp = CarInterface._get_params(CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW1),
|
||||
CAR.TESLA_MODEL_S_HW1, {0: {}}, [], True, False, False)
|
||||
controller = CarController(DBC[CAR.TESLA_MODEL_S_HW1], cp)
|
||||
state = CarState(cp, custom.StarPilotCarParams.new_message())
|
||||
state.out.vEgo = 10.
|
||||
cc = structs.CarControl.new_message()
|
||||
cc.longActive = True
|
||||
cc.cruiseControl.cancel = True
|
||||
cc.actuators.accel = 2.
|
||||
_, sends = controller.update(cc.as_reader(), state, 0, None)
|
||||
_, data, bus = next(msg for msg in sends if msg[0] == 0x2b9)
|
||||
assert bus == 0
|
||||
parser = CANParser("tesla_can", [(0x2b9, 0)], 0)
|
||||
parser.update([(1_000_000_000, [(0x2b9, data, bus)])])
|
||||
decoded = parser.vl["DAS_control"]
|
||||
assert decoded["DAS_accState"] == 13
|
||||
assert decoded["DAS_accelMin"] == pytest.approx(0, abs=0.05)
|
||||
assert decoded["DAS_accelMax"] == pytest.approx(0, abs=0.05)
|
||||
assert decoded["DAS_setSpeed"] != 200
|
||||
|
||||
|
||||
def test_hw1_carstate_uses_ap1_powertrain_and_chassis():
|
||||
cp = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW1)
|
||||
parsers = CarState.get_can_parsers(cp)
|
||||
frames = [
|
||||
(0x155, bytes.fromhex("000000000005e308"), 0), # ESP speed 15.07 kph
|
||||
(0x368, bytes.fromhex("84185e3009980a2d"), 0),
|
||||
(0x201, bytes.fromhex("5444008df2"), 0),
|
||||
]
|
||||
for parser in parsers.values():
|
||||
for addr in (0x155, 0x368, 0x201):
|
||||
_ = parser.vl[addr]
|
||||
parser.message_states[addr].ignore_counter = True
|
||||
parser.message_states[addr].ignore_checksum = True
|
||||
parser.update([(1_000_000_000, frames)])
|
||||
parser.update([(2_000_000_000, frames)])
|
||||
state = CarState(cp, custom.StarPilotCarParams.new_message())
|
||||
ret, _ = state.update(parsers, None)
|
||||
assert not ret.cruiseState.enabled
|
||||
assert ret.cruiseState.speed == pytest.approx(10 * 0.44704)
|
||||
assert ret.vEgoRaw == pytest.approx(15.07 / 3.6)
|
||||
assert not ret.seatbeltUnlatched
|
||||
|
||||
# A stale belt frame cannot allow an engagement indefinitely.
|
||||
for parser in parsers.values():
|
||||
parser.update([(4_000_000_000, [])])
|
||||
ret, _ = state.update(parsers, None)
|
||||
assert ret.seatbeltUnlatched
|
||||
|
||||
|
||||
def test_hw1_can_use_rcm_buckle_when_sdm1_is_absent():
|
||||
cp = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW1)
|
||||
parsers = CarState.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[CAR.TESLA_MODEL_S_HW1][Bus.chassis])
|
||||
addr, data, bus = packer.make_can_msg("RCM_status", 0, {"RCM_buckleDriverStatus": 1})
|
||||
assert addr == 0x211 and bus == 0
|
||||
frames = [(addr, data, bus)]
|
||||
for parser in parsers.values():
|
||||
_ = parser.vl["RCM_status"]
|
||||
parser.update([(1_000_000_000, frames)])
|
||||
state = CarState(cp, custom.StarPilotCarParams.new_message())
|
||||
out, _ = state.update(parsers, None)
|
||||
assert not out.seatbeltUnlatched
|
||||
@@ -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
|
||||
@@ -70,6 +70,16 @@ class CAR(Platforms):
|
||||
Bus.radar: 'tesla_radar_bosch_generated',
|
||||
},
|
||||
)
|
||||
TESLA_MODEL_S_HW1 = TeslaPlatformConfig(
|
||||
[CarDocs("Tesla Model S (with HW1) 2014-16", "All", support_type=SupportType.COMMUNITY, support_link="#community")],
|
||||
CarSpecs(mass=2100., wheelbase=2.960, steerRatio=15.0),
|
||||
{
|
||||
Bus.chassis: 'tesla_can',
|
||||
Bus.party: 'tesla_can',
|
||||
Bus.pt: 'tesla_can',
|
||||
Bus.radar: 'tesla_radar_bosch_generated',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
FW_QUERY_CONFIG = FwQueryConfig(
|
||||
@@ -125,15 +135,23 @@ class CarControllerParams:
|
||||
ACCEL_MAX = 2.0 # m/s^2
|
||||
ACCEL_MIN = -3.48 # m/s^2
|
||||
JERK_LIMIT_MAX = 4.9 # m/s^3, ACC faults at 5.0
|
||||
JERK_LIMIT_MIN = -4.9 # m/s^3, ACC faults at 5.0
|
||||
JERK_RAMP_RATE = JERK_LIMIT_MAX * 0.002
|
||||
|
||||
|
||||
class TeslaSafetyFlags(IntFlag):
|
||||
LONG_CONTROL = 1
|
||||
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:
|
||||
@@ -157,5 +175,7 @@ class CruiseButtons:
|
||||
|
||||
DBC = CAR.create_dbc_map()
|
||||
|
||||
LEGACY_CARS = (CAR.TESLA_MODEL_S_HW1,)
|
||||
|
||||
STEER_THRESHOLD = 1
|
||||
STEER_DISENGAGE_THRESHOLD = 5.0
|
||||
|
||||
@@ -35,6 +35,8 @@ non_tested_cars = [
|
||||
GM.CHEVROLET_MALIBU_ASCM,
|
||||
GM.CHEVROLET_MALIBU_SDGM,
|
||||
GM.CHEVROLET_SUBURBAN,
|
||||
GM.CHEVROLET_SUBURBAN_ASCM,
|
||||
GM.CHEVROLET_SUBURBAN_CAMERA,
|
||||
GM.CHEVROLET_TRAX,
|
||||
GM.CHEVROLET_VOLT_ASCM,
|
||||
GM.CHEVROLET_VOLT_CAMERA,
|
||||
@@ -108,6 +110,7 @@ non_tested_cars = [
|
||||
TOYOTA.TOYOTA_RAV4H,
|
||||
|
||||
# No recorded routes yet
|
||||
VOLVO.VOLVO_V40,
|
||||
VOLVO.VOLVO_XC40_RECHARGE,
|
||||
VOLVO.VOLVO_S60_RECHARGE,
|
||||
VOLVO.POLESTAR_2,
|
||||
|
||||
@@ -2,7 +2,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.car_helpers import FRAME_FINGERPRINT, _apply_starpilot_access_policy, _get_gm_stored_candidate_fallback, can_fingerprint
|
||||
from opendbc.car.car_helpers import FRAME_FINGERPRINT, _apply_starpilot_access_policy, _get_gm_stored_candidate_fallback, _normalize_gm_suburban_camera_candidate, can_fingerprint
|
||||
from opendbc.car.fingerprints import _FINGERPRINTS as FINGERPRINTS
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
@@ -116,3 +116,14 @@ class TestCanFingerprint:
|
||||
candidate = _apply_starpilot_access_policy("CHEVROLET_VOLT_CC", SimpleNamespace(block_user=True))
|
||||
|
||||
assert candidate == "CHEVROLET_VOLT_CC"
|
||||
|
||||
def test_gm_suburban_camera_variant_uses_vin_and_camera_bus_signature(self):
|
||||
fingerprints = {
|
||||
0: {190: 6, 201: 8, 209: 7, 211: 2, 241: 6, 304: 1, 320: 3},
|
||||
2: {0x24b: 8, 0x64b: 8},
|
||||
}
|
||||
|
||||
assert _normalize_gm_suburban_camera_candidate(None, fingerprints, "1GNSKJKJXKR148371") == "CHEVROLET_SUBURBAN_CAMERA"
|
||||
assert _normalize_gm_suburban_camera_candidate("GMC_YUKON", fingerprints, "1GNSKJKJXKR148371") == "CHEVROLET_SUBURBAN_CAMERA"
|
||||
assert _normalize_gm_suburban_camera_candidate(None, fingerprints, "1GNSKCKC5KR255194") is None
|
||||
assert _normalize_gm_suburban_camera_candidate(None, {0: fingerprints[0], 2: {0x320: 3}}, "1GNSKJKJXKR148371") is None
|
||||
|
||||
@@ -26,6 +26,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"TESLA_MODEL_3" = [nan, 2.5, nan]
|
||||
"TESLA_MODEL_Y" = [nan, 2.5, nan]
|
||||
"TESLA_MODEL_X" = [nan, 2.5, nan]
|
||||
"TESLA_MODEL_S_HW1" = [nan, 2.5, nan]
|
||||
|
||||
# Guess
|
||||
"VOLKSWAGEN_ID3_MK1" = [nan, 2.5, nan]
|
||||
@@ -145,6 +146,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2]
|
||||
"VOLVO_XC40_RECHARGE" = [1.5, 1.5, 0.1]
|
||||
"VOLVO_S60_RECHARGE" = [1.5, 1.5, 0.1]
|
||||
"VOLVO_V40" = [1.5, 1.5, 0.1]
|
||||
|
||||
# Dashcam or fallback configured as ideal car
|
||||
"MOCK" = [10.0, 10, 0.0]
|
||||
|
||||
@@ -90,6 +90,8 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"CHEVROLET_MALIBU" = "CHEVROLET_VOLT"
|
||||
"CHEVROLET_MALIBU_ASCM" = "CHEVROLET_VOLT"
|
||||
"CHEVROLET_MALIBU_SDGM" = "CHEVROLET_VOLT"
|
||||
"CHEVROLET_SUBURBAN_ASCM" = "CHEVROLET_SUBURBAN"
|
||||
"CHEVROLET_SUBURBAN_CAMERA" = "CHEVROLET_SUBURBAN"
|
||||
"HOLDEN_ASTRA" = "CHEVROLET_VOLT"
|
||||
"CHEVROLET_VOLT_CC" = "CHEVROLET_VOLT"
|
||||
"CHEVROLET_VOLT_CAMERA" = "CHEVROLET_VOLT"
|
||||
|
||||
@@ -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
|
||||
@@ -40,11 +40,14 @@ TOYOTA_NO_LEAD_CRUISE_SIGN_FLIP_MIN_SET_SPEED_ERROR = 0.35 # m/s
|
||||
TOYOTA_RAV4_LAUNCH_PEDAL_BLEND_SPEED = 5.0 # m/s
|
||||
TOYOTA_RAV4_LAUNCH_PEDAL_SCALE = 0.11
|
||||
TOYOTA_RAV4_LOW_SPEED_PEDAL_SCALE = 0.23
|
||||
TOYOTA_AUTO_HOLD_ACCEL = -1.0
|
||||
TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES = 100
|
||||
|
||||
# LKA limits
|
||||
# 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 = 18 # tx control frames needed before torque can be cut
|
||||
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
|
||||
@@ -65,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
|
||||
@@ -75,6 +83,14 @@ def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool:
|
||||
) or highlander_sdsu)
|
||||
|
||||
|
||||
def get_toyota_lat_active(requested_active: bool, steering_torque: float) -> bool:
|
||||
return requested_active and abs(steering_torque) < MAX_USER_TORQUE
|
||||
|
||||
|
||||
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:
|
||||
return (
|
||||
auto_hold_enabled and
|
||||
@@ -96,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.]
|
||||
@@ -242,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 ***
|
||||
@@ -266,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:
|
||||
@@ -309,8 +326,24 @@ class CarController(CarControllerBase):
|
||||
|
||||
self.last_standstill = CS.out.standstill
|
||||
|
||||
def update_auto_hold_state(self, CS: structs.CarState, cancel_requested: bool = False,
|
||||
activation_frames: int = TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES):
|
||||
brake_hold_allowed = (not cancel_requested and 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 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):
|
||||
can_sends = []
|
||||
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))
|
||||
@@ -323,16 +356,21 @@ class CarController(CarControllerBase):
|
||||
self.brake_hold_active = False
|
||||
|
||||
if self.frame % 2 == 0:
|
||||
can_sends.append(toyotacan.create_brake_hold_command(self.packer, self.frame, CS.pre_collision_2, self.brake_hold_active))
|
||||
return [toyotacan.create_brake_hold_command(self.packer, self.frame, CS.pre_collision_2, self.brake_hold_active)]
|
||||
return []
|
||||
|
||||
return can_sends
|
||||
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
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
actuators = CC.actuators
|
||||
stopping = actuators.longControlState == LongCtrlState.stopping
|
||||
hud_control = CC.hudControl
|
||||
pcm_cancel_cmd = CC.cruiseControl.cancel
|
||||
lat_active = CC.latActive and abs(CS.out.steeringTorque) < MAX_USER_TORQUE
|
||||
lat_active = get_toyota_lat_active(CC.latActive, CS.out.steeringTorque)
|
||||
|
||||
if len(CC.orientationNED) == 3:
|
||||
self.pitch.update(CC.orientationNED[1])
|
||||
@@ -359,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,
|
||||
)
|
||||
|
||||
@@ -423,10 +461,12 @@ 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)):
|
||||
can_sends.extend(self.create_auto_brake_hold_messages(CS))
|
||||
elif self.brake_hold_active:
|
||||
self._brake_hold_counter = 0
|
||||
self.brake_hold_active = False
|
||||
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()
|
||||
|
||||
interceptor_gas_cmd = self._compute_interceptor_gas_cmd(CC, CS)
|
||||
|
||||
@@ -534,6 +574,11 @@ 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 and not uses_toyota_auto_hold_aeb(self.CP):
|
||||
pcm_accel_cmd = TOYOTA_AUTO_HOLD_ACCEL
|
||||
self.permit_braking = True
|
||||
self.standstill_req = True
|
||||
|
||||
main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd
|
||||
can_sends.append(toyotacan.create_accel_command(self.packer, main_accel_cmd, pcm_cancel_cmd, self.permit_braking, self.standstill_req, lead,
|
||||
CS.acc_type, fcw_alert, self.distance_button,
|
||||
|
||||
@@ -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,7 +91,10 @@ 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)
|
||||
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:
|
||||
@@ -314,7 +318,8 @@ 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:
|
||||
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 {
|
||||
|
||||
@@ -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]
|
||||
@@ -164,8 +165,10 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.GAS_INTERCEPTOR.value
|
||||
|
||||
toyota_auto_hold = Params(return_defaults=True).get_bool("ToyotaAutoHold")
|
||||
if toyota_auto_hold and candidate in TOYOTA_AUTO_HOLD_CARS:
|
||||
ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
if toyota_auto_hold and ret.openpilotLongitudinalControl and candidate in TOYOTA_AUTO_HOLD_CARS:
|
||||
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,11 +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_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, \
|
||||
@@ -22,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
|
||||
@@ -81,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')]
|
||||
@@ -189,15 +230,23 @@ 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)
|
||||
car_params = CarInterface.get_params(
|
||||
candidate,
|
||||
{bus: {} for bus in range(8)},
|
||||
[],
|
||||
{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,
|
||||
@@ -207,21 +256,36 @@ class TestToyotaInterfaces:
|
||||
params.remove("ToyotaAutoHold")
|
||||
|
||||
assert car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
|
||||
assert 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" 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,
|
||||
@@ -229,7 +293,9 @@ 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(
|
||||
@@ -732,6 +798,44 @@ class TestToyotaFingerprint:
|
||||
|
||||
|
||||
class TestToyotaCarController:
|
||||
@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)
|
||||
|
||||
@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_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):
|
||||
controller = CarController.__new__(CarController)
|
||||
@@ -744,6 +848,8 @@ class TestToyotaCarController:
|
||||
controller.standstill_req = standstill_req
|
||||
controller.last_standstill = last_standstill
|
||||
controller.accel = 0.0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
return controller
|
||||
|
||||
@staticmethod
|
||||
@@ -806,9 +912,49 @@ class TestToyotaCarController:
|
||||
def test_toyota_auto_hold_latches_after_brake_press_until_gas(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
cruiseState=SimpleNamespace(available=True, enabled=False),
|
||||
gasPressed=False,
|
||||
brakePressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
)
|
||||
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert controller.brake_hold_active
|
||||
|
||||
cs.out.brakePressed = False
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert controller.brake_hold_active
|
||||
|
||||
cs.out.gasPressed = True
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert not controller.brake_hold_active
|
||||
|
||||
def test_toyota_auto_hold_does_not_trigger_without_brake_press(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
cruiseState=SimpleNamespace(available=True, enabled=False),
|
||||
gasPressed=False,
|
||||
brakePressed=False,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
)
|
||||
|
||||
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
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
@@ -821,38 +967,13 @@ class TestToyotaCarController:
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
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
|
||||
|
||||
cs.out.brakePressed = False
|
||||
controller.frame = 2
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
assert controller.brake_hold_active
|
||||
|
||||
cs.out.gasPressed = True
|
||||
controller.frame = 4
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
assert not controller.brake_hold_active
|
||||
|
||||
def test_toyota_auto_hold_does_not_trigger_without_brake_press(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
controller.frame = 0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
cruiseState=SimpleNamespace(available=True, enabled=False),
|
||||
gasPressed=False,
|
||||
brakePressed=False,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
assert not 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)
|
||||
@@ -991,12 +1112,9 @@ class TestToyotaCarController:
|
||||
assert parser.can_valid
|
||||
assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 1
|
||||
|
||||
def test_auto_brake_hold_sends_modified_pre_collision_after_timer(self):
|
||||
def test_auto_hold_uses_acc_control_brake_path(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
controller.frame = 0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
@@ -1005,16 +1123,19 @@ class TestToyotaCarController:
|
||||
brakePressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
can_sends = controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
can_sends = [toyotacan.create_accel_command(
|
||||
controller.packer, -1.0, False, True, True, False, 1, False, 0, False,
|
||||
)]
|
||||
|
||||
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("PRE_COLLISION_2", 0)], 0)
|
||||
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("ACC_CONTROL", 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
|
||||
assert parser.vl["ACC_CONTROL"]["ACCEL_CMD"] == -1.0
|
||||
assert parser.vl["ACC_CONTROL"]["PERMIT_BRAKING"] == 1
|
||||
assert parser.vl["ACC_CONTROL"]["RELEASE_STANDSTILL"] == 0
|
||||
|
||||
def test_interceptor_stop_and_go_holds_small_launch_at_standstill(self):
|
||||
controller = self._make_controller()
|
||||
@@ -1028,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
|
||||
@@ -1160,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):
|
||||
|
||||
@@ -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 += [
|
||||
|
||||
@@ -185,6 +185,29 @@ def volkswagen_mqb_meb_checksum(address: int, sig, d: bytearray) -> int:
|
||||
return crc ^ 0xFF
|
||||
|
||||
|
||||
def volkswagen_mqb_meb_dyn_len_checksum(address: int, sig, d: bytearray, length: int, const: list[int]) -> int:
|
||||
d = d[:length]
|
||||
crc = 0xFF
|
||||
for i in range(1, len(d)):
|
||||
crc ^= d[i]
|
||||
crc = CRC8H2F[crc]
|
||||
counter = d[1] & 0x0F
|
||||
crc ^= const[counter]
|
||||
crc = CRC8H2F[crc]
|
||||
return crc ^ 0xFF
|
||||
|
||||
|
||||
def volkswagen_meb_alt_crc_checksum(address: int, sig, d: bytearray) -> int:
|
||||
entry = VOLKSWAGEN_MEB_ALT_CRC_CONSTANTS.get(address)
|
||||
if entry:
|
||||
length, const = entry
|
||||
checksum = volkswagen_mqb_meb_dyn_len_checksum(address, sig, d, length, const)
|
||||
if checksum == d[0]:
|
||||
return checksum
|
||||
|
||||
return volkswagen_mqb_meb_checksum(address, sig, d)
|
||||
|
||||
|
||||
def xor_checksum(address: int, sig, d: bytearray, initial_value: int = 0) -> int:
|
||||
checksum = initial_value
|
||||
checksum_byte = sig.start_bit // 8
|
||||
@@ -243,6 +266,8 @@ VOLKSWAGEN_MQB_MEB_CONSTANTS: dict[int, list[int]] = {
|
||||
0xC5, 0x91, 0x0F, 0x27, 0x34, 0x04, 0x7F, 0x02], # EA_02
|
||||
0x20A: [0x9D, 0xE8, 0x36, 0xA1, 0xCA, 0x3B, 0x1D, 0x33,
|
||||
0xE0, 0xD5, 0xBB, 0x5F, 0xAE, 0x3C, 0x31, 0x9F], # EML_06
|
||||
0x25D: [0xDA, 0x6B, 0x0E, 0xB2, 0x78, 0xBD, 0x5A, 0x81,
|
||||
0x7B, 0xD6, 0x41, 0x39, 0x76, 0xB6, 0xD7, 0x35], # KLR_01
|
||||
0x26B: [0xCE, 0xCC, 0xBD, 0x69, 0xA1, 0x3C, 0x18, 0x76,
|
||||
0x0F, 0x04, 0xF2, 0x3A, 0x93, 0x24, 0x19, 0x51], # TA_01
|
||||
0x30C: [0x0F] * 16, # ACC_02
|
||||
@@ -256,3 +281,19 @@ VOLKSWAGEN_MQB_MEB_CONSTANTS: dict[int, list[int]] = {
|
||||
0x65D: [0xAC, 0xB3, 0xAB, 0xEB, 0x7A, 0xE1, 0x3B, 0xF7,
|
||||
0x73, 0xBA, 0x7C, 0x9E, 0x06, 0x5F, 0x02, 0xD9], # ESP_20
|
||||
}
|
||||
|
||||
|
||||
VOLKSWAGEN_MEB_ALT_CRC_CONSTANTS: dict[int, tuple[int, list[int]]] = {
|
||||
0x0DB: (42, [0x09, 0xFA, 0xCA, 0x8E, 0x62, 0xD5, 0xD1, 0xF0,
|
||||
0x31, 0xA0, 0xAF, 0xDA, 0x4D, 0x1A, 0x0A, 0x97]), # AWV_03
|
||||
0xFC: (60, [0x69, 0xDC, 0xF9, 0x64, 0x6A, 0xCE, 0x55, 0x2C,
|
||||
0xC4, 0x38, 0x8F, 0xD1, 0xC6, 0x43, 0xB4, 0xB1]), # ESC_51
|
||||
0x102: (44, [0xD7, 0x12, 0x85, 0x7E, 0x0B, 0x34, 0xFA, 0x16,
|
||||
0x7A, 0x25, 0x2D, 0x8F, 0x04, 0x8E, 0x5D, 0x35]), # ESC_50
|
||||
0x10B: (44, [0x2C, 0xB1, 0x1A, 0x75, 0xBB, 0x65, 0x79, 0x47,
|
||||
0x81, 0x2B, 0xCC, 0x96, 0x17, 0xDB, 0xC0, 0x94]), # Motor_51
|
||||
0x139: (28, [0x96, 0x92, 0x95, 0xB5, 0x6E, 0xE3, 0xBD, 0xB4,
|
||||
0xFA, 0xAE, 0xBE, 0xCB, 0xCF, 0xA5, 0x77, 0xEF]), # VMM_02
|
||||
0x13D: (28, [0x18, 0x71, 0x10, 0x8D, 0xD7, 0xAA, 0xB0, 0x78,
|
||||
0xAC, 0x12, 0xAE, 0x0C, 0xDD, 0xF1, 0x85, 0x68]), # QFK_01
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
@@ -1,10 +1,16 @@
|
||||
import random
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.can.packer import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.volkswagen.interface import CarInterface
|
||||
from opendbc.car.volkswagen.values import CAR, FW_QUERY_CONFIG, WMI, VolkswagenFlags, VolkswagenSafetyFlags
|
||||
from opendbc.car.volkswagen.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.volkswagen.mqbcan import volkswagen_meb_alt_crc_checksum, volkswagen_mqb_meb_checksum
|
||||
from opendbc.car.volkswagen.radar_interface import RadarInterface
|
||||
from opendbc.car.volkswagen.values import CAR, DBC, FW_QUERY_CONFIG, WMI, CanBus, VolkswagenFlags, VolkswagenSafetyFlags
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
@@ -60,6 +66,47 @@ class TestVolkswagenPlatformConfigs:
|
||||
assert not cp.pcmCruise
|
||||
assert cp.safetyConfigs[-1].safetyParam & VolkswagenSafetyFlags.LONG_CONTROL
|
||||
|
||||
@pytest.mark.parametrize("data_hex", (
|
||||
"fc03fcfcfc0f0000",
|
||||
"e304fcfcfc0f0000",
|
||||
"1105fcfcfc0f0000",
|
||||
))
|
||||
def test_meb_klr_checksum(self, data_hex):
|
||||
data = bytearray.fromhex(data_hex)
|
||||
assert volkswagen_mqb_meb_checksum(0x25D, None, data) == data[0]
|
||||
|
||||
@pytest.mark.parametrize(("address", "data_hex"), (
|
||||
(0x0DB, "bb0ffcf0fefe0000fd0fffc0ff0000000200000000000000010000000000000000000000000000000000000000000000"),
|
||||
(0x0FC, "650b1f007ef0b10c0000000000000000ffff1019191c1cfefe0000000000000000e0fff40140ffeb7f0748e481af421f00000000000000000000000000000000"),
|
||||
(0x102, "9f0e7cfa010500000020cb0402000000b703a00000ec0f00000000002cd3ff1f0020a60000000020000000007d5256ab"),
|
||||
(0x10B, "9d06000000007efe000000010000ff01feff000000000000000000000090240000000000000000000000000000000000"),
|
||||
(0x139, "ac0e850b0890132000d019800000000000000000000000003002000500000000"),
|
||||
(0x13D, "2412111101d1060000d0d410d106000000000000000000000000000000000000"),
|
||||
))
|
||||
def test_meb_gen2_checksum(self, address, data_hex):
|
||||
data = bytearray.fromhex(data_hex)
|
||||
assert volkswagen_meb_alt_crc_checksum(address, None, data) == data[0]
|
||||
|
||||
def test_meb_camera_radar_tracks(self):
|
||||
cp = self._get_meb_params(CAR.SKODA_ENYAQ_MK1)
|
||||
radar = RadarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.radar])
|
||||
message = packer.make_can_msg("MEB_Distance_01", CanBus(cp).cam, {
|
||||
"Distance_Status": 0,
|
||||
"Same_Lane_01_ObjectID": 1,
|
||||
"Same_Lane_01_Long_Distance": 25.0,
|
||||
"Same_Lane_01_Lat_Distance": 0.5,
|
||||
"Same_Lane_01_Rel_Velo": -2.0,
|
||||
})
|
||||
|
||||
radar_data = radar.update([(1_000_000_000, [message])])
|
||||
assert radar_data is not None
|
||||
assert len(radar_data.points) == 1
|
||||
assert radar_data.points[0].trackId == 0
|
||||
assert radar_data.points[0].dRel == pytest.approx(25.0, abs=0.1)
|
||||
assert radar_data.points[0].yRel == pytest.approx(0.5, abs=0.1)
|
||||
assert radar_data.points[0].vRel == pytest.approx(-2.0, abs=0.1)
|
||||
|
||||
def test_taos_longitudinal_actuator_delay(self):
|
||||
taos_cp = CarInterface.get_non_essential_params(CAR.VOLKSWAGEN_TAOS_MK1)
|
||||
golf_cp = CarInterface.get_non_essential_params(CAR.VOLKSWAGEN_GOLF_MK7)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
|
||||
from opendbc.can.packer import CANPacker
|
||||
@@ -5,16 +7,20 @@ from opendbc.car import Bus
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.lateral import apply_std_steer_angle_limits
|
||||
from opendbc.car.volvo.helpers import LCA3CounterSync
|
||||
from opendbc.car.volvo.volvocan import (create_lca_message, create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message,
|
||||
create_lca_5_message, create_lca_6_message, create_lca_7_message, create_pscm_related_message)
|
||||
from opendbc.car.volvo.values import CarControllerParams
|
||||
from opendbc.car.volvo.volvocan import (create_c1_cancel, create_c1_pscm_message, create_c1_steering_control, create_lca_message,
|
||||
create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message, create_lca_5_message,
|
||||
create_lca_6_message, create_lca_7_message, create_pscm_related_message)
|
||||
from opendbc.car.volvo.values import CAR, CarControllerParams, VolvoC1PlatformConfig
|
||||
|
||||
|
||||
class CarController(CarControllerBase):
|
||||
def __init__(self, dbc_names, CP):
|
||||
super().__init__(dbc_names, CP)
|
||||
self.packer = CANPacker(dbc_names[Bus.party])
|
||||
self.is_c1 = isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig)
|
||||
self.packer = CANPacker(dbc_names[Bus.pt] if self.is_c1 else dbc_names[Bus.party])
|
||||
self.apply_angle_last = 0.0 # Track last applied steering angle
|
||||
self.c1_torque_samples = deque(maxlen=CarControllerParams.C1_N_ZERO_TORQUE)
|
||||
self.c1_recovery_until = -1
|
||||
|
||||
self.gear_acc = 60
|
||||
self.lca_4_acc = 0 # Bresenham accumulator for 29 Hz
|
||||
@@ -62,6 +68,9 @@ class CarController(CarControllerBase):
|
||||
self.lca_auth_drv_mag_filt = 0.0
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
if self.is_c1:
|
||||
return self._update_c1(CC, CS)
|
||||
|
||||
can_sends = []
|
||||
actuators = CC.actuators
|
||||
|
||||
@@ -161,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)))
|
||||
@@ -241,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))
|
||||
@@ -285,3 +292,50 @@ class CarController(CarControllerBase):
|
||||
self.frame += 1
|
||||
self.last_lat_active = CC.latActive
|
||||
return new_actuators, can_sends
|
||||
|
||||
def _update_c1(self, CC, CS):
|
||||
can_sends = []
|
||||
actuators = CC.actuators
|
||||
|
||||
if self.frame % 2 == 0: # stock FSM1 and PSCM1 messages are 50 Hz
|
||||
requested_active = CC.latActive and CS.out.vEgo > self.CP.minSteerSpeed
|
||||
recovering = requested_active and self.frame < self.c1_recovery_until
|
||||
|
||||
if not requested_active:
|
||||
self.c1_torque_samples.clear()
|
||||
self.c1_recovery_until = -1
|
||||
elif recovering:
|
||||
self.c1_torque_samples.clear()
|
||||
else:
|
||||
if self.c1_recovery_until >= 0:
|
||||
self.c1_recovery_until = -1
|
||||
self.c1_torque_samples.clear()
|
||||
self.c1_torque_samples.append(CS.c1_lka_torque)
|
||||
if (len(self.c1_torque_samples) == CarControllerParams.C1_N_ZERO_TORQUE and
|
||||
all(torque == 0 for torque in self.c1_torque_samples)):
|
||||
self.c1_recovery_until = self.frame + 100
|
||||
self.c1_torque_samples.clear()
|
||||
recovering = True
|
||||
|
||||
lat_active = requested_active and not recovering
|
||||
desired_angle = float(np.clip(
|
||||
actuators.steeringAngleDeg,
|
||||
CS.out.steeringAngleDeg - CarControllerParams.C1_ANGLE_ERROR,
|
||||
CS.out.steeringAngleDeg + CarControllerParams.C1_ANGLE_ERROR,
|
||||
))
|
||||
apply_angle = apply_std_steer_angle_limits(
|
||||
desired_angle, self.apply_angle_last, CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg, lat_active, CarControllerParams.C1_ANGLE_LIMITS,
|
||||
)
|
||||
|
||||
can_sends.append(create_c1_pscm_message(self.packer, CS.c1_msg_pscm))
|
||||
can_sends.append(create_c1_steering_control(self.packer, apply_angle, lat_active))
|
||||
self.apply_angle_last = apply_angle
|
||||
|
||||
if CC.cruiseControl.cancel and self.frame % 10 == 0:
|
||||
can_sends.append(create_c1_cancel(self.packer))
|
||||
|
||||
new_actuators = actuators.as_builder()
|
||||
new_actuators.steeringAngleDeg = self.apply_angle_last
|
||||
self.frame += 1
|
||||
return new_actuators, can_sends
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from cereal import custom
|
||||
from opendbc.car import structs, Bus
|
||||
from opendbc.car import Bus, ButtonType, create_button_events, structs
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.volvo.values import DBC, VolvoSPAPlatformConfig, CAR
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import CarStateBase
|
||||
from opendbc.car.volvo.values import CAR, DBC, VolvoC1PlatformConfig, VolvoSPAPlatformConfig
|
||||
|
||||
GearShifter = structs.CarState.GearShifter
|
||||
TransmissionType = structs.CarParams.TransmissionType
|
||||
@@ -16,6 +17,7 @@ STEERING_PRESSED_THRESHOLD = 2
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP, FPCP):
|
||||
super().__init__(CP, FPCP)
|
||||
self.is_c1 = isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig)
|
||||
self.is_spa = isinstance(CAR(CP.carFingerprint).config, VolvoSPAPlatformConfig)
|
||||
self.gas_pressed_prev = False
|
||||
self.dispatch_lca_2_msg = False
|
||||
@@ -34,8 +36,22 @@ class CarState(CarStateBase):
|
||||
self.msg_lca_4 = {}
|
||||
self.msg_lca_6 = {}
|
||||
self.msg_lca_7 = {}
|
||||
self.c1_msg_pscm = {}
|
||||
self.c1_lka_torque = 0
|
||||
self.c1_button_states = {
|
||||
"ACCOnOffBtn": False,
|
||||
"ACCStopBtn": False,
|
||||
"ACCSetBtn": False,
|
||||
"ACCResumeBtn": False,
|
||||
"ACCMinusBtn": False,
|
||||
"TimeGapIncreaseBtn": False,
|
||||
"TimeGapDecreaseBtn": False,
|
||||
}
|
||||
|
||||
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
|
||||
if self.is_c1:
|
||||
return self._update_c1(can_parsers)
|
||||
|
||||
cp_main = can_parsers[Bus.main]
|
||||
cp_pt = can_parsers[Bus.pt]
|
||||
cp_party = can_parsers[Bus.party]
|
||||
@@ -137,8 +153,83 @@ class CarState(CarStateBase):
|
||||
fp_ret = custom.StarPilotCarState.new_message()
|
||||
return ret, fp_ret
|
||||
|
||||
def _update_c1(self, can_parsers):
|
||||
cp = can_parsers[Bus.pt]
|
||||
cp_cam = can_parsers[Bus.cam]
|
||||
ret = structs.CarState()
|
||||
|
||||
ret.vEgoRaw = cp.vl["VehicleSpeed1"]["VehicleSpeed"] * CV.KPH_TO_MS
|
||||
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
|
||||
ret.standstill = ret.vEgoRaw < 0.1
|
||||
|
||||
ret.steeringAngleDeg = cp.vl["PSCM1"]["SteeringAngleServo"]
|
||||
ret.steeringTorque = cp.vl["PSCM1"]["LKATorque"]
|
||||
ret.steeringPressed = False
|
||||
|
||||
ret.gasPressed = cp.vl["PedalandBrake"]["AccPedal"] > 5.0
|
||||
ret.brakePressed = bool(cp.vl["PedalandBrake"]["BrakePedalActive2"] or
|
||||
cp.vl["PedalandBrake"]["BrakePedalActive"])
|
||||
|
||||
ret.gearShifter = {
|
||||
0: GearShifter.park,
|
||||
1: GearShifter.reverse,
|
||||
2: GearShifter.neutral,
|
||||
3: GearShifter.drive,
|
||||
}.get(int(cp.vl["TCM0"]["GearShifter"]), GearShifter.unknown)
|
||||
|
||||
ret.cruiseState.available = bool(cp_cam.vl["FSM0"]["ACCStatusOnOff"])
|
||||
ret.cruiseState.enabled = bool(cp_cam.vl["FSM0"]["ACCStatusActive"])
|
||||
ret.cruiseState.speed = cp.vl["ACC"]["SpeedTargetACC"] * CV.KPH_TO_MS
|
||||
ret.cruiseState.nonAdaptive = False
|
||||
ret.cruiseState.standstill = ret.standstill
|
||||
|
||||
turn_signal = int(cp.vl["MiscCarInfo"]["TurnSignal"])
|
||||
ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(
|
||||
50, turn_signal == 1, turn_signal == 3)
|
||||
ret.doorOpen = False
|
||||
ret.seatbeltUnlatched = False
|
||||
|
||||
button_types = {
|
||||
"ACCOnOffBtn": ButtonType.mainCruise,
|
||||
"ACCStopBtn": ButtonType.cancel,
|
||||
"ACCSetBtn": ButtonType.setCruise,
|
||||
"ACCResumeBtn": ButtonType.resumeCruise,
|
||||
"ACCMinusBtn": ButtonType.decelCruise,
|
||||
"TimeGapIncreaseBtn": ButtonType.gapAdjustCruise,
|
||||
"TimeGapDecreaseBtn": ButtonType.gapAdjustCruise,
|
||||
}
|
||||
button_events = []
|
||||
for signal, button_type in button_types.items():
|
||||
pressed = bool(cp.vl["CCButtons"][signal])
|
||||
button_events.extend(create_button_events(pressed, self.c1_button_states[signal], {True: button_type}))
|
||||
self.c1_button_states[signal] = pressed
|
||||
ret.buttonEvents = button_events
|
||||
|
||||
self.c1_msg_pscm = cp.vl["PSCM1"]
|
||||
self.c1_lka_torque = int(cp.vl["PSCM1"]["LKATorque"])
|
||||
|
||||
fp_ret = custom.StarPilotCarState.new_message()
|
||||
return ret, fp_ret
|
||||
|
||||
@staticmethod
|
||||
def get_can_parsers(CP):
|
||||
if isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig):
|
||||
return {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [
|
||||
("VehicleSpeed1", 50),
|
||||
("CCButtons", 100),
|
||||
("PSCM1", 50),
|
||||
("PedalandBrake", 100),
|
||||
("TCM0", 10),
|
||||
("ACC", 17),
|
||||
("MiscCarInfo", 25),
|
||||
], 0),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.cam], [
|
||||
("FSM0", 100),
|
||||
("FSM1", 50),
|
||||
], 2),
|
||||
}
|
||||
|
||||
return {
|
||||
Bus.main: CANParser(DBC[CP.carFingerprint][Bus.main], [], 0),
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 1),
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
from opendbc.car.volvo.values import CAR
|
||||
|
||||
FINGERPRINTS = {
|
||||
CAR.VOLVO_V40: [
|
||||
# V40 2017
|
||||
{8: 8, 16: 8, 48: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 208: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 352: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 624: 8, 640: 8, 648: 8, 652: 8, 656: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 832: 8, 848: 8, 853: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1061: 8, 1072: 8, 1409: 8},
|
||||
# V40 2015
|
||||
{8: 8, 16: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 648: 8, 652: 8, 656: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 832: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1061: 8, 1072: 8, 1409: 8},
|
||||
# V40 2014
|
||||
{8: 8, 16: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 648: 8, 652: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1072: 8, 1409: 8},
|
||||
],
|
||||
CAR.VOLVO_XC40_RECHARGE: [{
|
||||
21: 8, 22: 8, 23: 8, 26: 8, 69: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 128: 8, 144: 8, 146: 8, 147: 8, 151: 8, 304: 8, 336: 8, 339: 8, 341: 8, 394: 8, 395: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 1072: 8, 1120: 8, 1302: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8
|
||||
}],
|
||||
|
||||
@@ -2,11 +2,10 @@ from opendbc.car import structs, get_safety_config
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from opendbc.car.volvo.carcontroller import CarController
|
||||
from opendbc.car.volvo.carstate import CarState
|
||||
from opendbc.car.volvo.values import VolvoSPAPlatformConfig, CAR
|
||||
from opendbc.car.volvo.values import CAR, VolvoC1PlatformConfig, VolvoSafetyFlags, VolvoSPAPlatformConfig
|
||||
|
||||
TransmissionType = structs.CarParams.TransmissionType
|
||||
|
||||
VOLVO_FLAG_SPA = 1
|
||||
SAFETY_VOLVO = structs.CarParams.SafetyModel.volvo
|
||||
|
||||
|
||||
@@ -18,17 +17,20 @@ class CarInterface(CarInterfaceBase):
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
ret.brand = 'volvo'
|
||||
|
||||
platform = CAR(candidate).config
|
||||
safety_param = 0
|
||||
if isinstance(CAR(candidate).config, VolvoSPAPlatformConfig):
|
||||
safety_param = VOLVO_FLAG_SPA
|
||||
if isinstance(platform, VolvoSPAPlatformConfig):
|
||||
safety_param = VolvoSafetyFlags.SPA.value
|
||||
elif isinstance(platform, VolvoC1PlatformConfig):
|
||||
safety_param = VolvoSafetyFlags.C1.value
|
||||
ret.safetyConfigs = [get_safety_config(SAFETY_VOLVO, safety_param)]
|
||||
#ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
|
||||
|
||||
ret.dashcamOnly = False
|
||||
|
||||
ret.steerActuatorDelay = 0.3
|
||||
ret.steerActuatorDelay = 0.2 if isinstance(platform, VolvoC1PlatformConfig) else 0.3
|
||||
ret.steerLimitTimer = 0.1
|
||||
ret.steerAtStandstill = True
|
||||
ret.steerAtStandstill = not isinstance(platform, VolvoC1PlatformConfig)
|
||||
|
||||
# Use angle-based steering control for Volvo CMA platform
|
||||
ret.steerControlType = structs.CarParams.SteerControlType.angle
|
||||
@@ -39,4 +41,7 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
ret.pcmCruise = True
|
||||
|
||||
if isinstance(platform, VolvoC1PlatformConfig):
|
||||
ret.transmissionType = TransmissionType.automatic
|
||||
|
||||
return ret
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import pytest
|
||||
|
||||
from cereal import custom
|
||||
from opendbc.can.packer import CANPacker
|
||||
from opendbc.car import Bus, ButtonType, CanData, structs
|
||||
from opendbc.car.volvo.carstate import CarState
|
||||
from opendbc.car.volvo.interface import CarInterface
|
||||
from opendbc.car.volvo.values import CAR, DBC
|
||||
|
||||
|
||||
def _can_data(msg):
|
||||
address, data, bus = msg
|
||||
return CanData(address, data, bus)
|
||||
|
||||
|
||||
def test_c1_carstate_decodes_vehicle_and_cruise_signals():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
cs = CarState(cp, custom.StarPilotCarParams.new_message())
|
||||
parsers = CarState.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
|
||||
messages = [
|
||||
packer.make_can_msg("VehicleSpeed1", 0, {"VehicleSpeed": 72}),
|
||||
packer.make_can_msg("CCButtons", 0, {"ACCStopBtn": 1, "ACCSetBtn": 1}),
|
||||
packer.make_can_msg("PSCM1", 0, {"SteeringAngleServo": -12.5, "LKATorque": 7}),
|
||||
packer.make_can_msg("PedalandBrake", 0, {"AccPedal": 6, "BrakePedalActive2": 1}),
|
||||
packer.make_can_msg("TCM0", 0, {"GearShifter": 3}),
|
||||
packer.make_can_msg("ACC", 0, {"SpeedTargetACC": 100}),
|
||||
packer.make_can_msg("MiscCarInfo", 0, {"TurnSignal": 1}),
|
||||
packer.make_can_msg("FSM0", 2, {"ACCStatusOnOff": 1, "ACCStatusActive": 1}),
|
||||
packer.make_can_msg("FSM1", 2, {}),
|
||||
]
|
||||
packets = [(1_000_000, [_can_data(msg) for msg in messages])]
|
||||
for parser in parsers.values():
|
||||
parser.update(packets)
|
||||
|
||||
ret, _ = cs.update(parsers, None)
|
||||
assert ret.vEgoRaw == pytest.approx(20.0)
|
||||
assert ret.steeringAngleDeg == pytest.approx(-12.5, abs=0.05)
|
||||
assert ret.steeringTorque == 7
|
||||
assert ret.gasPressed and ret.brakePressed
|
||||
assert ret.gearShifter == structs.CarState.GearShifter.drive
|
||||
assert ret.cruiseState.available and ret.cruiseState.enabled
|
||||
assert ret.cruiseState.speed == pytest.approx(100 / 3.6)
|
||||
assert ret.leftBlinker and not ret.rightBlinker
|
||||
assert len(ret.buttonEvents) == 2
|
||||
assert any(event.type == ButtonType.cancel and event.pressed for event in ret.buttonEvents)
|
||||
assert any(event.type == ButtonType.setCruise and event.pressed for event in ret.buttonEvents)
|
||||
|
||||
release = packer.make_can_msg("CCButtons", 0, {})
|
||||
packets = [(2_000_000, [_can_data(release)])]
|
||||
for parser in parsers.values():
|
||||
parser.update(packets)
|
||||
ret, _ = cs.update(parsers, None)
|
||||
assert len(ret.buttonEvents) == 2
|
||||
assert any(event.type == ButtonType.cancel and not event.pressed for event in ret.buttonEvents)
|
||||
assert any(event.type == ButtonType.setCruise and not event.pressed for event in ret.buttonEvents)
|
||||
@@ -1,10 +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 DBC
|
||||
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():
|
||||
@@ -52,18 +57,129 @@ 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():
|
||||
return SimpleNamespace(
|
||||
out=SimpleNamespace(steeringAngleDeg=10.0, vEgo=12.0, vEgoRaw=12.0),
|
||||
c1_lka_torque=5,
|
||||
c1_msg_pscm=_zero_message(),
|
||||
)
|
||||
|
||||
|
||||
def test_c1_controller_emits_checked_steering_and_pscm_relay():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cc = SimpleNamespace(
|
||||
latActive=True,
|
||||
actuators=_Actuators(),
|
||||
cruiseControl=SimpleNamespace(cancel=False),
|
||||
)
|
||||
|
||||
actuators, can_sends = controller.update(cc, _c1_state(), 0, None)
|
||||
assert [(msg[0], msg[2]) for msg in can_sends] == [(0x125, 2), (0xD0, 0)]
|
||||
|
||||
fsm = can_sends[1][1]
|
||||
assert fsm[7] & 0x3 == 3
|
||||
assert fsm[6] == create_c1_checksum(fsm)
|
||||
assert 0.0 < actuators.steeringAngleDeg <= 2.0
|
||||
|
||||
|
||||
def test_c1_controller_sends_only_cancel_button():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cc = SimpleNamespace(
|
||||
latActive=False,
|
||||
actuators=_Actuators(),
|
||||
cruiseControl=SimpleNamespace(cancel=True),
|
||||
)
|
||||
|
||||
_, can_sends = controller.update(cc, _c1_state(), 0, None)
|
||||
buttons = next(msg for msg in can_sends if msg[0] == 0x10)
|
||||
assert buttons[2] == 0
|
||||
assert buttons[1][7] == 0x10
|
||||
assert buttons[1][6] == 0
|
||||
|
||||
|
||||
def test_c1_controller_temporarily_drops_steering_on_zero_torque_fault():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cs = _c1_state()
|
||||
cs.c1_lka_torque = 0
|
||||
cc = SimpleNamespace(
|
||||
latActive=True,
|
||||
actuators=_Actuators(),
|
||||
cruiseControl=SimpleNamespace(cancel=False),
|
||||
)
|
||||
|
||||
directions = []
|
||||
for _ in range(23):
|
||||
_, can_sends = controller.update(cc, cs, 0, None)
|
||||
directions.extend(msg[1][7] & 0x3 for msg in can_sends if msg[0] == 0xD0)
|
||||
|
||||
assert directions[:-1] == [3] * 11
|
||||
assert directions[-1] == 0
|
||||
|
||||
while controller.frame <= 122:
|
||||
_, can_sends = controller.update(cc, cs, 0, None)
|
||||
fsm = next(msg for msg in can_sends if msg[0] == 0xD0)
|
||||
assert fsm[1][7] & 0x3 == 3
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntFlag
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
|
||||
from opendbc.car.lateral import AngleSteeringLimits
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts
|
||||
from opendbc.car.fw_query_definitions import FwQueryConfig
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
# C1 support is adapted from the original dragonpilot V40 port:
|
||||
# https://github.com/dragonpilot/dragonpilot/commit/773dce507082d931236b64dca8024dce9625446f
|
||||
|
||||
|
||||
class VolvoSafetyFlags(IntFlag):
|
||||
SPA = 1
|
||||
C1 = 2
|
||||
|
||||
|
||||
class CarControllerParams:
|
||||
STEER_STEP = 1 # 100 Hz LCA command frequency (controlsd runs at 100 Hz)
|
||||
@@ -61,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)
|
||||
@@ -81,6 +89,19 @@ class CarControllerParams:
|
||||
([0., 5., 25.], [5., 2., .3]), # rate down limits at different speeds
|
||||
)
|
||||
|
||||
C1_STEER_NO = 0
|
||||
C1_STEER = 3
|
||||
C1_N_ZERO_TORQUE = 12
|
||||
C1_ANGLE_ERROR = 20.0
|
||||
C1_ANGLE_DELTA_BP = [0., 8.33, 13.89, 19.44, 25., 30.55, 36.1]
|
||||
C1_ANGLE_DELTA_UP = [2., 1.2, .25, .20, .15, .10, .10]
|
||||
C1_ANGLE_DELTA_DOWN = [2., 1.2, .25, .20, .15, .10, .10]
|
||||
C1_ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits(
|
||||
359.9,
|
||||
(C1_ANGLE_DELTA_BP, C1_ANGLE_DELTA_UP),
|
||||
(C1_ANGLE_DELTA_BP, C1_ANGLE_DELTA_DOWN),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolvoCarDocs(CarDocs):
|
||||
@@ -105,7 +126,26 @@ class VolvoSPAPlatformConfig(PlatformConfig):
|
||||
})
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolvoC1PlatformConfig(PlatformConfig):
|
||||
dbc_dict: DbcDict = field(default_factory=lambda: {
|
||||
Bus.pt: 'volvo_v40_2017_pt',
|
||||
Bus.cam: 'volvo_v40_2017_pt',
|
||||
})
|
||||
|
||||
|
||||
class CAR(Platforms):
|
||||
VOLVO_V40 = VolvoC1PlatformConfig(
|
||||
[VolvoCarDocs("Volvo V40 2013-19")],
|
||||
CarSpecs(
|
||||
mass=1610,
|
||||
wheelbase=2.647,
|
||||
steerRatio=14.7,
|
||||
centerToFrontRatio=0.44,
|
||||
minSteerSpeed=1.0 * CV.KPH_TO_MS,
|
||||
),
|
||||
)
|
||||
|
||||
VOLVO_XC40_RECHARGE = VolvoCMAPlatformConfig(
|
||||
[VolvoCarDocs("Volvo XC40 Recharge 2021-23")],
|
||||
CarSpecs(
|
||||
|
||||
@@ -2,6 +2,47 @@ from opendbc.car.volvo.helpers import (checksum_lca_2_message, checksum_2_0x69_m
|
||||
checksum_2_pscm_related_message, checksum_lca_5_message)
|
||||
from opendbc.car.carlog import carlog
|
||||
|
||||
|
||||
def create_c1_pscm_message(packer, msg_pscm: dict):
|
||||
values = {
|
||||
"LKATorque": 0,
|
||||
"SteeringAngleServo": msg_pscm["SteeringAngleServo"],
|
||||
"byte0": msg_pscm["byte0"],
|
||||
"byte3": msg_pscm["byte3"],
|
||||
"byte4": msg_pscm["byte4"],
|
||||
"byte7": msg_pscm["byte7"],
|
||||
"LKAActive": int(msg_pscm["LKAActive"]) & 0xD,
|
||||
}
|
||||
return packer.make_can_msg("PSCM1", 2, values)
|
||||
|
||||
|
||||
def create_c1_checksum(data: bytes) -> int:
|
||||
angle_raw = ((data[4] & 0x3F) << 8) | data[5]
|
||||
direction = data[7] & 0x3
|
||||
checksum_sum = (data[3] + direction + angle_raw + (angle_raw >> 8)) & 0xFF
|
||||
return checksum_sum ^ 0xFF
|
||||
|
||||
|
||||
def create_c1_steering_control(packer, apply_angle: float, lat_active: bool):
|
||||
values = {
|
||||
"SET_X_E3": 0xE3,
|
||||
"SET_X_B4": 0xB4,
|
||||
"SET_X_08": 0x08,
|
||||
"TrqLim": 0,
|
||||
"LKAAngleReq": apply_angle,
|
||||
"LKASteerDirection": 3 if lat_active else 0,
|
||||
"SET_X_25": 0x25,
|
||||
"SET_X_02": 0x02,
|
||||
}
|
||||
data = packer.make_can_msg("FSM1", 0, values)[1]
|
||||
values["Checksum"] = create_c1_checksum(data)
|
||||
return packer.make_can_msg("FSM1", 0, values)
|
||||
|
||||
|
||||
def create_c1_cancel(packer):
|
||||
return packer.make_can_msg("CCButtons", 0, {"ACCStopBtn": 1})
|
||||
|
||||
|
||||
def create_lca_message(packer, lat_active: bool, apply_angle: float, msg_lca: dict,
|
||||
authority_pos: int = 614, authority_neg: int = -614,
|
||||
overrides: dict | None = None):
|
||||
@@ -209,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
|
||||
|
||||
@@ -220,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";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user