mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-02 04:13:46 +08:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52999cb7b2 | |||
| 1e6b221d53 | |||
| 11900616bf | |||
| a60333e513 | |||
| 9d87c4c8cb | |||
| a45ecf73ab | |||
| 452dc42868 | |||
| faf5b53321 | |||
| ac8028a21e | |||
| 96ef704da7 | |||
| 8d01d881cb | |||
| 7d2f012387 | |||
| da7af42e49 | |||
| b1e7a5c46e | |||
| 7b4643208e | |||
| 2dd44a6368 | |||
| 19b2264e43 | |||
| a19ed91f61 | |||
| 04c2353096 | |||
| 6cae0cebe7 | |||
| ba901b5f55 | |||
| e6a60d6cba | |||
| d638e62811 | |||
| 18465ed4ef | |||
| f5672221a6 | |||
| 99c5efa680 | |||
| 1648a100e3 | |||
| d3a74f61e4 | |||
| 0c6ee69362 | |||
| aaf1061111 | |||
| 79c61f479a | |||
| 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 |
@@ -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
|
||||
@@ -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.
+19
-1
@@ -198,7 +198,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"AggressiveJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
|
||||
{"AlertVolumeControl", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"AllowImpossibleAcceleration", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"AlwaysOnLateral", {PERSISTENT, BOOL, "1", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"AlwaysOnLateral", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"AlwaysOnLateralLKAS", {PERSISTENT, BOOL, "1", "0", 2}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON, "{}", "{}"}},
|
||||
{"AutomaticallyDownloadModels", {PERSISTENT, BOOL, "1", "0", 1}},
|
||||
@@ -318,6 +318,7 @@ 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}},
|
||||
{"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}},
|
||||
@@ -444,6 +445,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}},
|
||||
@@ -623,6 +625,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}},
|
||||
@@ -632,6 +638,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}},
|
||||
@@ -693,6 +700,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}},
|
||||
@@ -725,8 +741,10 @@ 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}},
|
||||
{"TeslaCoopSteering", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
|
||||
{"TetheringEnabled", {PERSISTENT, INT, "0", "0", 0}},
|
||||
|
||||
Binary file not shown.
@@ -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.
|
||||
+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
|
||||
|
||||
@@ -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
|
||||
@@ -196,6 +196,8 @@ def get_checksum_state(dbc_name: str) -> ChecksumState | None:
|
||||
return ChecksumState(8, -1, 7, -1, False, SignalType.TOYOTA_CHECKSUM, toyota_checksum)
|
||||
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,7 +64,9 @@ 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
|
||||
@@ -165,7 +167,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,12 +183,11 @@ 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.
|
||||
@@ -210,7 +211,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))
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -63,6 +63,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
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from opendbc.car import Bus, gen_empty_fingerprint
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car.ford import fordcan
|
||||
from opendbc.car.ford.carcontroller import FordStockCruiseButton
|
||||
from opendbc.car.ford.carcontroller import FordStockCruiseButton, apply_creep_compensation
|
||||
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.fw_versions import build_fw_dict
|
||||
@@ -38,6 +38,17 @@ def test_stock_cruise_button_ignores_press_with_cruise_master_off():
|
||||
assert button.update(True, cruise_available=False, cruise_enabled=False) == (False, False)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
ECU_ADDRESSES = {
|
||||
Ecu.eps: 0x730, # Power Steering Control Module (PSCM)
|
||||
Ecu.abs: 0x760, # Anti-Lock Brake System (ABS)
|
||||
@@ -192,10 +203,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):
|
||||
|
||||
@@ -1159,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):
|
||||
@@ -1191,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)
|
||||
|
||||
@@ -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,8 +31,8 @@ BOLT_CC_DIRECTION_MEMORY_S = 1.5
|
||||
VOLT_CC_CARS = {
|
||||
CAR.CHEVROLET_VOLT_CC,
|
||||
}
|
||||
VOLT_CC_TARGET_DEADBAND_MPH = 5.0
|
||||
VOLT_CC_ACCEL_DEADBAND_MS2 = 0.15
|
||||
VOLT_CC_FREE_REQUEST_DEADBAND_MPH = 5.0
|
||||
VOLT_CC_ACTIVE_REQUEST_DEADBAND_MPH = 2.0
|
||||
|
||||
|
||||
def malibu_phase_map_for_button(button):
|
||||
@@ -341,20 +341,37 @@ 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 = v_cruise_kph if is_metric else v_cruise_kph * CV.KPH_TO_MPH
|
||||
target_deadband = VOLT_CC_TARGET_DEADBAND_MPH * (CV.MPH_TO_KPH if is_metric else 1.0)
|
||||
if abs(target_setpoint - speed_setpoint) <= target_deadband:
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
target_setpoint = int(round(v_cruise_kph if is_metric else v_cruise_kph * CV.KPH_TO_MPH))
|
||||
|
||||
if abs(accel) <= VOLT_CC_ACCEL_DEADBAND_MS2:
|
||||
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")
|
||||
|
||||
if accel < 0.0:
|
||||
@@ -371,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
|
||||
@@ -386,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
|
||||
|
||||
@@ -306,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
|
||||
@@ -522,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)
|
||||
|
||||
|
||||
@@ -28,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
|
||||
|
||||
@@ -268,6 +268,94 @@ class TestGMCarState:
|
||||
|
||||
|
||||
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,
|
||||
@@ -919,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,
|
||||
@@ -935,19 +1023,19 @@ 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=0.5), SimpleNamespace(is_metric=True),
|
||||
packer, controller, cs, SimpleNamespace(accel=1.0), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
|
||||
def test_volt_cc_redneck_holds_when_stock_setpoint_is_within_target_deadband(self):
|
||||
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(
|
||||
@@ -960,7 +1048,7 @@ class TestGMCarController:
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=100.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=99.0 * CV.KPH_TO_MS),
|
||||
cruiseState=SimpleNamespace(speed=100.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
@@ -970,11 +1058,11 @@ class TestGMCarController:
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
assert controller.apply_speed == 99
|
||||
assert controller.apply_speed == 100
|
||||
|
||||
def test_volt_cc_redneck_catches_up_when_target_exceeds_deadband(self):
|
||||
def test_volt_cc_redneck_tracks_max_inside_request_deadband(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(3.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
@@ -984,25 +1072,175 @@ class TestGMCarController:
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=90.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=90.0 * CV.KPH_TO_MS),
|
||||
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.5), SimpleNamespace(is_metric=True),
|
||||
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.7 / DT_CTRL)
|
||||
controller.frame = int(0.3 / DT_CTRL)
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
|
||||
packer, controller, cs, SimpleNamespace(accel=2.0), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 91
|
||||
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),
|
||||
@@ -547,6 +555,7 @@ CAMERA_ACC_CAR = {
|
||||
CAR.CHEVROLET_SILVERADO,
|
||||
CAR.CHEVROLET_EQUINOX,
|
||||
CAR.CHEVROLET_TRAILBLAZER,
|
||||
CAR.CHEVROLET_SUBURBAN_CAMERA,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
CAR.CHEVROLET_BLAZER,
|
||||
CAR.CHEVROLET_TRAX,
|
||||
@@ -554,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 = {
|
||||
@@ -593,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,
|
||||
|
||||
@@ -4,19 +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_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
|
||||
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
|
||||
@@ -41,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]
|
||||
@@ -437,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)
|
||||
@@ -474,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
|
||||
@@ -483,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:
|
||||
@@ -503,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
|
||||
@@ -632,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
|
||||
@@ -757,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:
|
||||
@@ -775,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(
|
||||
@@ -798,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
|
||||
@@ -808,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:
|
||||
@@ -836,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)):
|
||||
@@ -993,7 +1043,9 @@ 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)
|
||||
adrv_messages = hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame,
|
||||
car_fingerprint=self.CP.carFingerprint,
|
||||
drive_gear=drive_gear)
|
||||
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.
|
||||
@@ -1022,14 +1074,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:
|
||||
scc_jerk_limits = get_hyundai_canfd_scc_jerk_limits(self.CP)
|
||||
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,
|
||||
|
||||
@@ -138,6 +138,9 @@ 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)
|
||||
if CP.carFingerprint == CAR.KIA_RAY_EV:
|
||||
self.ray_pedal_state = 5
|
||||
self.ray_pedal_valid = False
|
||||
|
||||
self.cruise_info = {}
|
||||
self.msg_161 = {}
|
||||
@@ -300,6 +303,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 +397,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 +413,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):
|
||||
@@ -748,4 +763,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
|
||||
|
||||
@@ -1556,6 +1556,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,33 @@ 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):
|
||||
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)
|
||||
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 +150,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:
|
||||
@@ -699,13 +731,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
|
||||
@@ -783,15 +815,13 @@ 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):
|
||||
# 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))
|
||||
|
||||
if blended_hda2:
|
||||
return ret
|
||||
|
||||
@@ -2,6 +2,7 @@ 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, \
|
||||
@@ -43,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:
|
||||
@@ -199,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
|
||||
@@ -302,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:
|
||||
@@ -317,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
|
||||
@@ -378,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)
|
||||
@@ -20,13 +20,13 @@ 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
|
||||
suppress_redundant_gv70_brake_cancel
|
||||
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
|
||||
get_canfd_cruise_available
|
||||
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX, get_communication_control_request
|
||||
from opendbc.car.hyundai import hyundaican, hyundaicanfd
|
||||
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, 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, \
|
||||
@@ -148,6 +148,60 @@ class TestHyundaiFingerprint:
|
||||
|
||||
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)
|
||||
@@ -498,6 +552,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
|
||||
|
||||
@@ -625,14 +686,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"
|
||||
@@ -726,6 +780,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)
|
||||
@@ -748,6 +841,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])
|
||||
@@ -951,6 +1086,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
|
||||
@@ -1156,6 +1316,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):
|
||||
@@ -1495,8 +1752,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()
|
||||
@@ -1539,6 +1796,20 @@ class TestHyundaiFingerprint:
|
||||
assert exact
|
||||
assert matches == {candidate}
|
||||
|
||||
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
|
||||
|
||||
@@ -2535,7 +2806,7 @@ 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)
|
||||
@@ -2580,11 +2851,11 @@ 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
|
||||
|
||||
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)
|
||||
@@ -2608,6 +2879,27 @@ 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_gv70_electrified_stock_long_uses_damped_lkas_request(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
|
||||
|
||||
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),
|
||||
@@ -2684,10 +2976,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),
|
||||
)
|
||||
|
||||
@@ -2700,7 +2993,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)
|
||||
@@ -2995,6 +3289,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
|
||||
|
||||
@@ -233,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))
|
||||
@@ -241,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
|
||||
@@ -270,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
|
||||
|
||||
@@ -45,6 +46,7 @@ class CarController(CarControllerBase):
|
||||
self.angle_override_confirm_frames = 0
|
||||
self.angle_lkas_active = False
|
||||
self.angle_handoff_active = False
|
||||
self.ascent_angle_initialized = False
|
||||
self.ascent_aol_arm_frames = 0
|
||||
|
||||
self.cruise_button_prev = 0
|
||||
@@ -69,6 +71,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.
|
||||
@@ -130,14 +133,12 @@ class CarController(CarControllerBase):
|
||||
self.angle_override_confirm_frames = 0
|
||||
self.angle_handoff_active = False
|
||||
|
||||
def _angle_manual_handoff(self, CS, lat_active, use_steering_pressed=False):
|
||||
def _angle_manual_handoff(self, CS, lat_active):
|
||||
if not lat_active:
|
||||
self._reset_angle_handoff()
|
||||
return False
|
||||
|
||||
driver_override = self._update_angle_driver_override(CS)
|
||||
if use_steering_pressed:
|
||||
driver_override = driver_override or getattr(CS.out, "steeringPressed", False)
|
||||
steering_rate = abs(getattr(CS.out, "steeringRateDeg", 0.0))
|
||||
if driver_override:
|
||||
self.angle_handoff_active = True
|
||||
@@ -204,6 +205,10 @@ class CarController(CarControllerBase):
|
||||
return subarucan.create_steering_control_angle(self.packer, apply_steer, lkas_active, self.angle_bus)
|
||||
|
||||
if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
||||
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023 and not self.ascent_angle_initialized:
|
||||
self.apply_steer_last = CS.out.steeringAngleDeg
|
||||
self.ascent_angle_initialized = True
|
||||
|
||||
mads_only = CC.latActive and not CC.enabled
|
||||
mads_only_ok = CS.out.vEgoRaw > _ANGLE_MADS_MIN_SPEED and \
|
||||
abs(CS.out.steeringAngleDeg) < _ANGLE_MADS_MAX_STEER_ANGLE
|
||||
@@ -216,33 +221,24 @@ class CarController(CarControllerBase):
|
||||
else:
|
||||
self.ascent_aol_arm_frames = _ASCENT_AOL_ARM_FRAMES if lkas_available else 0
|
||||
|
||||
manual_handoff = self._angle_manual_handoff(
|
||||
CS, lkas_available, use_steering_pressed=self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023,
|
||||
)
|
||||
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
|
||||
|
||||
if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
||||
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,
|
||||
)
|
||||
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)
|
||||
@@ -319,6 +315,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:
|
||||
@@ -380,9 +383,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, self._lkas_status_active(CC), 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,
|
||||
|
||||
@@ -4,8 +4,9 @@ from opendbc.can import CANDefine, CANParser
|
||||
from opendbc.car import Bus, create_button_events, structs
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import CarStateBase
|
||||
from opendbc.car.subaru.values import DBC, CanBus, SUBARU_REDNECK_CRUISE_CARS, SUBARU_STOP_START_CARS, SubaruFlags
|
||||
from opendbc.car.subaru.values import CAR, DBC, CanBus, SUBARU_REDNECK_CRUISE_CARS, SUBARU_STOP_START_CARS, SubaruFlags
|
||||
from opendbc.car import CanSignalRateCalculator
|
||||
from opendbc.car.subaru.avh import INPUTS as AVH_INPUTS
|
||||
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
@@ -26,6 +27,7 @@ class CarState(CarStateBase):
|
||||
self.dashlights_msg = {}
|
||||
self.dashlights_dat = b""
|
||||
self.stop_start_state = 0
|
||||
self.avh_frames = {}
|
||||
self.cruise_buttons_msg = {}
|
||||
self.cruise_buttons = {button: 0 for button in SUBARU_CRUISE_BUTTONS}
|
||||
|
||||
@@ -37,6 +39,9 @@ class CarState(CarStateBase):
|
||||
cp_angle = cp_main if self.CP.flags & SubaruFlags.D_PLATFORM else cp
|
||||
ret = structs.CarState()
|
||||
|
||||
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
|
||||
self.avh_frames = {a: (cp_alt.ts_nanos[a]["CHECKSUM"] / 1e9, cp_alt.vl_raw[a]) for a in AVH_INPUTS}
|
||||
|
||||
if self.CP.carFingerprint in SUBARU_STOP_START_CARS:
|
||||
stop_start_cp = cp_alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
|
||||
self.dashlights_msg = copy.copy(stop_start_cp.vl["Dashlights"])
|
||||
@@ -177,11 +182,15 @@ class CarState(CarStateBase):
|
||||
|
||||
@staticmethod
|
||||
def get_can_parsers(CP):
|
||||
avh_messages = [(a, 0) for a in (0x6BB, 0x32B, 0x40, 0x48)] if CP.carFingerprint == CAR.SUBARU_LEGACY_2025 else []
|
||||
parsers = {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main_for_cp(CP)),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.camera),
|
||||
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.alt_for_cp(CP))
|
||||
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], avh_messages, CanBus.alt_for_cp(CP))
|
||||
}
|
||||
if CP.flags & SubaruFlags.D_PLATFORM:
|
||||
parsers[Bus.main] = CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main)
|
||||
if CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
|
||||
for address in AVH_INPUTS:
|
||||
parsers[Bus.alt].vl[address]
|
||||
return parsers
|
||||
|
||||
@@ -42,6 +42,8 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
|
||||
if candidate in SUBARU_STOP_START_CARS:
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
|
||||
if candidate == CAR.SUBARU_LEGACY_2025:
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.AVH_STARTUP.value
|
||||
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.subaru.avh import AVH_REQUEST, AVH_STATUS, INPUTS, AvhStartup, avh_request, checksum
|
||||
from opendbc.car.subaru.carcontroller import CarController
|
||||
from opendbc.car.subaru.interface import CarInterface
|
||||
from opendbc.car.subaru.values import CAR, SubaruSafetyFlags
|
||||
from opendbc.car import Bus
|
||||
|
||||
|
||||
def sample(address, counter):
|
||||
data = bytearray(8)
|
||||
data[1] = counter & 15
|
||||
if address == AVH_REQUEST:
|
||||
data[3], data[5], data[6] = 1, 0x80, 0x0E # captured Legacy payload, not Outback constants
|
||||
elif address == 0x40:
|
||||
data[2:4] = (800).to_bytes(2, 'little')
|
||||
elif address == 0x48:
|
||||
data[3] = 4
|
||||
elif address == 0x174:
|
||||
data[2] = 8
|
||||
data[0] = checksum(address, data)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def prepare(fast_counter_step=1):
|
||||
policy = AvhStartup()
|
||||
frames = {}
|
||||
for tick in range(101):
|
||||
now = 100 + tick / 10
|
||||
for address in INPUTS:
|
||||
if address != AVH_REQUEST or tick % 10 == 0:
|
||||
counter = tick // 10 if address == AVH_REQUEST else tick * (1 if address == AVH_STATUS else fast_counter_step)
|
||||
frames[address] = (now, sample(address, counter))
|
||||
sent = policy.update(now, frames, True, True, False)
|
||||
if tick < 100:
|
||||
assert sent == []
|
||||
assert sent == [avh_request(frames[AVH_REQUEST][1], 1)]
|
||||
return policy, frames
|
||||
|
||||
|
||||
def test_captured_legacy_press_bytes():
|
||||
template = bytes.fromhex('5b0b000100800e00')
|
||||
assert avh_request(template, 1) == (0x6BB, bytes.fromhex('5e0c020100800e00'), 1)
|
||||
assert avh_request(template, 2) == (0x6BB, bytes.fromhex('5f0d020100800e00'), 1)
|
||||
wrap = sample(AVH_REQUEST, 15)
|
||||
assert avh_request(wrap, 1)[1][1] == 0
|
||||
assert avh_request(wrap, 2)[1][1] == 1
|
||||
|
||||
|
||||
def test_two_frames_only_and_no_retry():
|
||||
policy, frames = prepare()
|
||||
assert policy.update(110.04, frames, True, True, False) == []
|
||||
assert policy.update(110.06, frames, True, True, False) == [avh_request(frames[AVH_REQUEST][1], 2)]
|
||||
assert policy.update(110.07, frames, True, True, False) == []
|
||||
assert policy.update(111, frames, True, True, False) == []
|
||||
|
||||
|
||||
def test_controller_snapshots_may_skip_fast_can_samples():
|
||||
policy, frames = prepare(fast_counter_step=2)
|
||||
assert policy.update(110.06, frames, True, True, False) == [avh_request(frames[AVH_REQUEST][1], 2)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reason', ['late', 'new_template', 'manual', 'ack', 'moving', 'gas', 'gear', 'invalid', 'disabled', 'engaged', 'stale'])
|
||||
def test_followup_aborts_permanently(reason):
|
||||
policy, frames = prepare()
|
||||
address, offset, value = {
|
||||
'manual': (AVH_REQUEST, 2, 1), 'ack': (AVH_STATUS, 5, 32),
|
||||
'moving': (0x13A, 2, 1), 'gas': (0x40, 4, 1), 'gear': (0x48, 3, 3),
|
||||
'new_template': (AVH_REQUEST, 1, 11),
|
||||
}.get(reason, (None, None, None))
|
||||
if address is not None:
|
||||
data = bytearray(frames[address][1])
|
||||
data[1] = (data[1] + 1) & 15
|
||||
data[offset] = value
|
||||
data[0] = checksum(address, data)
|
||||
frames[address] = (110.05, bytes(data))
|
||||
if reason == 'stale':
|
||||
frames[0x40] = (109, frames[0x40][1])
|
||||
now = 110.08 if reason == 'late' else 110.06
|
||||
assert policy.update(now, frames, reason != 'disabled', reason != 'invalid', reason == 'engaged') == []
|
||||
assert policy.done
|
||||
assert policy.update(111, frames, True, True, False) == []
|
||||
|
||||
|
||||
def test_only_legacy_has_avh_safety_permission():
|
||||
for car in CAR:
|
||||
cp = CarInterface.get_non_essential_params(car)
|
||||
assert bool(cp.safetyConfigs[0].safetyParam & SubaruSafetyFlags.AVH_STARTUP) == (car == CAR.SUBARU_LEGACY_2025)
|
||||
|
||||
|
||||
def test_existing_required_messages_keep_alive_checks():
|
||||
cp = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
|
||||
parser = CarInterface.CarState.get_can_parsers(cp)[Bus.alt]
|
||||
assert not parser.message_states[0x13A].ignore_alive
|
||||
assert not parser.message_states[0x174].ignore_alive
|
||||
assert parser.message_states[AVH_REQUEST].ignore_alive
|
||||
assert parser.message_states[AVH_STATUS].ignore_alive
|
||||
|
||||
|
||||
def test_controller_sends_only_when_opted_in():
|
||||
cp = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
|
||||
controller = CarController({}, cp)
|
||||
cc = SimpleNamespace(enabled=False, latActive=False, longActive=False,
|
||||
actuators=SimpleNamespace(as_builder=lambda: SimpleNamespace(steeringAngleDeg=0)),
|
||||
hudControl=SimpleNamespace(leadVisible=False), cruiseControl=SimpleNamespace(cancel=False))
|
||||
cs = SimpleNamespace(out=SimpleNamespace(canValid=True), avh_frames={})
|
||||
toggles = SimpleNamespace(subaru_stop_start_off=False, subaru_avh_on=False, subaru_sng=False)
|
||||
controller.frame = 1
|
||||
_, sent = controller.update(cc, cs, 100_000_000_000, toggles)
|
||||
assert not any(m[0] in (AVH_REQUEST, AVH_STATUS) for m in sent)
|
||||
@@ -643,8 +643,8 @@ def test_angle_controller_uses_fixed_angle_rate_limits(platform):
|
||||
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_reengages_immediately_after_manual_steering_stops(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))
|
||||
@@ -682,6 +682,28 @@ def test_angle_controller_reengages_immediately_after_manual_steering_stops(plat
|
||||
assert CS.out.steeringAngleDeg < parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] < CC.actuators.steeringAngleDeg
|
||||
|
||||
|
||||
def test_ascent_reentry_rate_uses_last_transmitted_angle():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
controller = CarController({}, CP)
|
||||
controller.angle_handoff_active = True
|
||||
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-1.45))
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=30.3, steeringAngleDeg=0.78, steeringRateDeg=-1.5, steeringTorque=56.0,
|
||||
gearShifter=structs.CarState.GearShifter.drive, standstill=False,
|
||||
))
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
|
||||
|
||||
parser.update([(1, [controller.lateral_angle(CC, CS)])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(0.78)
|
||||
|
||||
CS.out.steeringAngleDeg = 0.74
|
||||
CS.out.steeringRateDeg = -1.99
|
||||
parser.update([(2, [controller.lateral_angle(CC, CS)])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(0.53, abs=0.01)
|
||||
|
||||
|
||||
def test_ascent_angle_controller_waits_for_parking_lot_safety_envelope():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
controller = CarController({}, CP)
|
||||
@@ -775,7 +797,7 @@ def test_ascent_angle_controller_does_not_delay_normal_engagement():
|
||||
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, self._lkas_status_active(CC)" 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
|
||||
|
||||
|
||||
@@ -795,7 +817,7 @@ def test_lkas_hud_active_bit_follows_lateral_state(enabled, expected):
|
||||
assert parser.vl["ES_LKAS_State"]["LKAS_ACTIVE"] == expected
|
||||
|
||||
|
||||
def test_outback_manual_steering_releases_angle_request_before_lkas_fault():
|
||||
def test_outback_manual_steering_keeps_cooperative_angle_request():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_OUTBACK_2023)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(
|
||||
@@ -806,20 +828,45 @@ def test_outback_manual_steering_releases_angle_request_before_lkas_fault():
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=0.9,
|
||||
steeringAngleDeg=-57.0,
|
||||
steeringRateDeg=-45.0,
|
||||
steeringTorque=-127.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)
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(1, [msg])])
|
||||
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"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
assert not controller._lkas_status_active(CC)
|
||||
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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,10 @@ from opendbc.can import CANDefine, CANParser
|
||||
from opendbc.car import Bus, 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,
|
||||
)
|
||||
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
|
||||
@@ -25,8 +28,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
|
||||
@@ -75,6 +89,8 @@ class CarState(CarStateBase):
|
||||
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]
|
||||
@@ -173,10 +189,94 @@ class CarState(CarStateBase):
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 TeslaSafetyFlags, CAR, DBC, LEGACY_CARS
|
||||
from opendbc.car.tesla.preap.interface import get_preap_accel_limits, get_preap_params
|
||||
|
||||
|
||||
@@ -32,6 +32,21 @@ 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)]
|
||||
|
||||
ret.steerLimitTimer = 0.4
|
||||
|
||||
@@ -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
|
||||
@@ -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,10 +135,14 @@ 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
|
||||
|
||||
|
||||
@@ -157,5 +171,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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -11,7 +11,7 @@ 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
|
||||
UNSUPPORTED_DSU_CAR, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS, TOYOTA_AUTO_HOLD_AEB_CARS
|
||||
from opendbc.can import CANPacker
|
||||
|
||||
Ecu = structs.CarParams.Ecu
|
||||
@@ -46,7 +46,8 @@ 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
|
||||
@@ -77,6 +78,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
|
||||
@@ -244,6 +253,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 ***
|
||||
@@ -326,6 +336,22 @@ class CarController(CarControllerBase):
|
||||
|
||||
return self.brake_hold_active
|
||||
|
||||
def create_auto_brake_hold_messages(self, CS: structs.CarState, brake_hold_allowed_timer: int = 100):
|
||||
brake_hold_allowed = (CS.out.standstill and CS.out.cruiseState.available and
|
||||
not CS.out.gasPressed and not CS.out.cruiseState.enabled and
|
||||
CS.out.gearShifter not in (PARK, REVERSE))
|
||||
|
||||
if brake_hold_allowed and not self.brake_hold_active and CS.out.brakePressed:
|
||||
self._brake_hold_counter += 1
|
||||
self.brake_hold_active = self._brake_hold_counter > brake_hold_allowed_timer
|
||||
elif not brake_hold_allowed:
|
||||
self._brake_hold_counter = 0
|
||||
self.brake_hold_active = False
|
||||
|
||||
if self.frame % 2 == 0:
|
||||
return [toyotacan.create_brake_hold_command(self.packer, self.frame, CS.pre_collision_2, self.brake_hold_active)]
|
||||
return []
|
||||
|
||||
def reset_auto_hold_state(self):
|
||||
self._brake_hold_counter = 0
|
||||
self.brake_hold_active = False
|
||||
@@ -335,7 +361,7 @@ class CarController(CarControllerBase):
|
||||
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])
|
||||
@@ -362,7 +388,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,
|
||||
)
|
||||
|
||||
@@ -426,7 +452,10 @@ class CarController(CarControllerBase):
|
||||
|
||||
self._update_standstill_request(CC, CS, actuators, starpilot_toggles)
|
||||
if supports_toyota_auto_hold(self.CP, getattr(starpilot_toggles, "toyota_auto_hold", False)):
|
||||
self.update_auto_hold_state(CS, pcm_cancel_cmd)
|
||||
if self.CP.carFingerprint in TOYOTA_AUTO_HOLD_AEB_CARS:
|
||||
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()
|
||||
|
||||
@@ -536,7 +565,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX))
|
||||
|
||||
if self.brake_hold_active:
|
||||
if self.brake_hold_active and self.CP.carFingerprint not in TOYOTA_AUTO_HOLD_AEB_CARS:
|
||||
pcm_accel_cmd = TOYOTA_AUTO_HOLD_ACCEL
|
||||
self.permit_braking = True
|
||||
self.standstill_req = True
|
||||
|
||||
@@ -9,6 +9,7 @@ from opendbc.car.interfaces import CarStateBase
|
||||
from opendbc.car.toyota.values import ToyotaFlags, ToyotaStarPilotFlags, CAR, DBC, STEER_THRESHOLD, NO_STOP_TIMER_CAR, \
|
||||
TSS2_CAR, RADAR_ACC_CAR, EPS_SCALE, UNSUPPORTED_DSU_CAR, \
|
||||
SECOC_CAR, LEGACY_PRIUS_CAR
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
SteerControlType = structs.CarParams.SteerControlType
|
||||
@@ -90,6 +91,11 @@ class CarState(CarStateBase):
|
||||
self.has_can_filter = self.FPCP.flags & ToyotaStarPilotFlags.RADAR_CAN_FILTER.value
|
||||
self.has_SDSU = self.FPCP.flags & ToyotaStarPilotFlags.SMART_DSU.value
|
||||
self.has_ZSS = self.FPCP.flags & ToyotaStarPilotFlags.ZSS.value
|
||||
self.auto_brake_hold = bool(
|
||||
self.CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value and
|
||||
getattr(self.CP, "alternativeExperience", 0) & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
)
|
||||
self.pre_collision_2 = {}
|
||||
|
||||
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
|
||||
cp = can_parsers[Bus.pt]
|
||||
@@ -225,6 +231,9 @@ class CarState(CarStateBase):
|
||||
if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
|
||||
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD"])
|
||||
|
||||
if self.auto_brake_hold:
|
||||
self.pre_collision_2 = copy.copy(cp_cam.vl["PRE_COLLISION_2"])
|
||||
|
||||
if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR:
|
||||
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
|
||||
|
||||
@@ -309,6 +318,10 @@ class CarState(CarStateBase):
|
||||
if CP.carFingerprint in DISTANCE_BUTTON_CAR:
|
||||
pt_messages.append(("PCM_CRUISE_4", 1))
|
||||
|
||||
if (CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value and
|
||||
getattr(CP, "alternativeExperience", 0) & ALTERNATIVE_EXPERIENCE.ALLOW_AEB):
|
||||
cam_messages.append(("PRE_COLLISION_2", 50))
|
||||
|
||||
return {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2),
|
||||
|
||||
@@ -4,7 +4,8 @@ from opendbc.car.toyota.carcontroller import CarController
|
||||
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, \
|
||||
TOYOTA_AUTO_HOLD_AEB_CARS
|
||||
from opendbc.car.disable_ecu import disable_ecu
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
@@ -165,7 +166,9 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
toyota_auto_hold = Params(return_defaults=True).get_bool("ToyotaAutoHold")
|
||||
if toyota_auto_hold and ret.openpilotLongitudinalControl and candidate in TOYOTA_AUTO_HOLD_CARS:
|
||||
ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
ret.alternativeExperience |= (ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
if candidate in TOYOTA_AUTO_HOLD_AEB_CARS
|
||||
else ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD)
|
||||
ret.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value
|
||||
|
||||
if not ret.openpilotLongitudinalControl:
|
||||
|
||||
@@ -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, \
|
||||
TOYOTA_AUTO_HOLD_AEB_CARS, \
|
||||
get_platform_codes
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
from openpilot.common.params import Params
|
||||
@@ -208,13 +212,17 @@ class TestToyotaInterfaces:
|
||||
params.remove("ToyotaAutoHold")
|
||||
|
||||
assert car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
|
||||
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
if candidate in TOYOTA_AUTO_HOLD_AEB_CARS:
|
||||
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
else:
|
||||
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
|
||||
can_parsers = CarState.get_can_parsers(car_params)
|
||||
car_state = CarState(car_params, SimpleNamespace(flags=0))
|
||||
car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
|
||||
assert "PRE_COLLISION_2" not in can_parsers[Bus.cam].vl
|
||||
assert (0x344 in can_parsers[Bus.cam].vl) == (candidate in TOYOTA_AUTO_HOLD_AEB_CARS)
|
||||
|
||||
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H])
|
||||
def test_auto_hold_is_disabled_by_default(self, candidate):
|
||||
@@ -734,6 +742,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)
|
||||
@@ -848,6 +894,31 @@ class TestToyotaCarController:
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert not controller.brake_hold_active
|
||||
|
||||
def test_camry_auto_hold_uses_legacy_aeb_brake_path(self):
|
||||
controller = self._make_controller()
|
||||
controller.CP.carFingerprint = CAR.TOYOTA_CAMRY_TSS2
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
controller.frame = 0
|
||||
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
cruiseState=SimpleNamespace(available=True, enabled=False),
|
||||
gasPressed=False,
|
||||
brakePressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
can_sends = controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("PRE_COLLISION_2", 0)], 0)
|
||||
parser.update([(1, can_sends)])
|
||||
|
||||
assert controller.brake_hold_active
|
||||
assert parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -1.0
|
||||
assert parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 1
|
||||
|
||||
def test_prius_resume_request_releases_standstill_latch(self):
|
||||
controller = self._make_controller(standstill_req=True, last_standstill=True)
|
||||
|
||||
|
||||
@@ -89,6 +89,38 @@ def create_pcs_commands(packer, accel, active, mass):
|
||||
return [msg1, msg2]
|
||||
|
||||
|
||||
def create_brake_hold_command(packer, frame, pre_collision_2, brake_hold_active):
|
||||
values = {s: pre_collision_2[s] for s in [
|
||||
"DSS1GDRV",
|
||||
"DS1STAT2",
|
||||
"DS1STBK2",
|
||||
"PCSWAR",
|
||||
"PCSALM",
|
||||
"PCSOPR",
|
||||
"PCSABK",
|
||||
"PBATRGR",
|
||||
"PPTRGR",
|
||||
"IBTRGR",
|
||||
"CLEXTRGR",
|
||||
"IRLT_REQ",
|
||||
"BRKHLD",
|
||||
"AVSTRGR",
|
||||
"VGRSTRGR",
|
||||
"PREFILL",
|
||||
"PBRTRGR",
|
||||
"PCSDIS",
|
||||
"PBPREPMP",
|
||||
] if s in pre_collision_2}
|
||||
|
||||
if brake_hold_active:
|
||||
values = {
|
||||
"DSS1GDRV": 0x3FF,
|
||||
"PBRTRGR": frame % 730 < 727,
|
||||
}
|
||||
|
||||
return packer.make_can_msg("PRE_COLLISION_2", 0, values)
|
||||
|
||||
|
||||
def create_acc_cancel_command(packer):
|
||||
values = {
|
||||
"GAS_RELEASED": 0,
|
||||
|
||||
@@ -629,6 +629,10 @@ 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. Other
|
||||
# supported Toyota models use the ACC_CONTROL hold request.
|
||||
TOYOTA_AUTO_HOLD_AEB_CARS = {CAR.TOYOTA_CAMRY_TSS2}
|
||||
|
||||
# no resume button press required
|
||||
NO_STOP_TIMER_CAR = CAR.with_flags(ToyotaFlags.NO_STOP_TIMER)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -258,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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from opendbc.car import Bus
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.volkswagen.interface import CarInterface
|
||||
from opendbc.car.volkswagen.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.volkswagen.mqbcan import volkswagen_mqb_meb_checksum
|
||||
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
|
||||
|
||||
@@ -75,6 +75,18 @@ class TestVolkswagenPlatformConfigs:
|
||||
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)
|
||||
|
||||
@@ -170,8 +170,6 @@ class CarController(CarControllerBase):
|
||||
# convention = driver pushing right → yields right authority
|
||||
# (LOOSELY/+ arm), retains left (INV/- arm).
|
||||
# Yield arm scales with |drv| above OVERRIDE_THRESH — strong presses
|
||||
# (potholes, hard corrections) cross past zero so EPS hands the wheel
|
||||
# to the driver in their direction.
|
||||
excess = max(0.0, self.lca_auth_drv_mag_filt - float(P.LCA_AUTH_OVERRIDE_ENTER))
|
||||
yield_signed = float(P.LCA_AUTH_YIELD_BASE) - P.LCA_AUTH_YIELD_SLOPE * excess
|
||||
yield_signed = max(float(P.LCA_AUTH_YIELD_MIN), min(yield_signed, float(P.LCA_AUTH_YIELD_BASE)))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.volvo.carcontroller import CarController
|
||||
from opendbc.car.volvo.helpers import checksum_lca_5_message
|
||||
from opendbc.car.volvo.interface import CarInterface
|
||||
from opendbc.car.volvo.values import CAR, DBC
|
||||
from opendbc.car.volvo.volvocan import create_c1_checksum
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def _zero_message():
|
||||
@@ -70,6 +73,35 @@ def test_controller_relays_stock_lca5_angle_when_inactive():
|
||||
assert abs(raw * 0.05596 - 12.0) < 0.1
|
||||
|
||||
|
||||
@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),
|
||||
|
||||
@@ -71,14 +71,12 @@ class CarControllerParams:
|
||||
# (potholes, lane corrections) get full yield while light sustained pressure
|
||||
# only gets a soft yield. yield_signed = YIELD_BASE − YIELD_SLOPE *
|
||||
# max(0, drv_mag_filt − OVERRIDE_ENTER), clamped to [YIELD_MIN, YIELD_BASE].
|
||||
# At |drv|=7 (just over threshold): yield = +60 (light resistance).
|
||||
# At |drv|=14: yield ≈ -4 (crosses past zero — EPS hands wheel to driver).
|
||||
# drv_mag_filt is a low-pass of |drv| (alpha=0.04, ~250 ms time constant) —
|
||||
# without it, 1-2 unit driver-torque jitter became ~10 unit yield-arm jitter
|
||||
# which PSCM converted to felt ripple at sustained co-steering pressure.
|
||||
LCA_AUTH_YIELD_BASE = 60 # yield-arm magnitude at the override threshold
|
||||
LCA_AUTH_YIELD_SLOPE = 8 # counts of yield reduction per unit |drv torque| above threshold
|
||||
LCA_AUTH_YIELD_MIN = -30 # cap how far past zero the yield arm can go (full hand-over)
|
||||
LCA_AUTH_YIELD_MIN = 0 # yield authority without crossing the safety sign boundary
|
||||
LCA_AUTH_YIELD_LP_ALPHA = 0.04 # LP-filter coefficient on |drv| for yield calc (~250 ms tau)
|
||||
LCA_AUTH_SPLIT = 200 # symmetric → asymmetric handover
|
||||
LCA_AUTH_REBUILD_RATE = 230 # counts/s (≈ 2.7 s rebuild from 0 to 614)
|
||||
|
||||
@@ -727,10 +727,14 @@ BO_ 1259 LOCAL_TIME2: 8 XXX
|
||||
SG_ NEW_SIGNAL_3 : 39|1@0+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 1264 LOCAL_TIME: 8 XXX
|
||||
SG_ HOURS : 12|5@0+ (1,0) [0|31] "" XXX
|
||||
SG_ MINUTES : 21|6@0+ (1,0) [0|63] "" XXX
|
||||
SG_ SECONDS : 31|8@0+ (1,0) [0|59] "" XXX
|
||||
SG_ HOURS : 8|8@1+ (1,0) [0|23] "" XXX
|
||||
SG_ MINUTES : 16|8@1+ (1,0) [0|59] "" XXX
|
||||
SG_ SECONDS : 24|8@1+ (1,0) [0|59] "" XXX
|
||||
SG_ MONTH : 34|4@1+ (1,0) [1|12] "" XXX
|
||||
SG_ YEAR : 40|8@1+ (1,2000) [2000|2255] "" XXX
|
||||
SG_ DAY : 48|8@1+ (1,0) [1|31] "" XXX
|
||||
|
||||
CM_ BO_ 1264 "Cluster wall clock, 1Hz. Local time, not UTC. All 0xFF until the cluster initializes.";
|
||||
CM_ SG_ 96 BRAKE_PRESSURE "User applied brake pedal pressure. Ramps from computer applied pressure on falling edge of cruise. Cruise cancels if !=0";
|
||||
CM_ SG_ 101 BRAKE_POSITION "User applied brake pedal position, max is ~700. Signed on some vehicles";
|
||||
CM_ SG_ 203 ADAS_ActvACISta "ADAS Active AngleControlInterface State";
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
CM_ "IMPORT _subaru_global.dbc";
|
||||
|
||||
BO_ 1723 AVH_Request: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ REQUEST : 16|2@1+ (1,0) [0|3] "" XXX
|
||||
|
||||
BO_ 811 AVH_Status: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ ENABLED : 45|1@1+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 72 Transmission: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
|
||||
@@ -1480,6 +1480,7 @@ BO_ 905 SCC14: 8 SCC
|
||||
SG_ JerkLowerLimit : 19|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC
|
||||
SG_ ACCMode : 32|3@1+ (1,0) [0|7] "" CLU,HUD,LDWS_LKAS,ESC
|
||||
SG_ ObjGap : 56|8@1+ (1,0) [0|255] "" CLU,HUD,ESC
|
||||
SG_ ObjDistStat : 42|2@1+ (1,0) [0|3] "" XXX
|
||||
|
||||
BO_ 1157 LFAHDA_MFC: 8 XXX
|
||||
SG_ HDA_USM : 0|2@1+ (1,0) [0|3] "" XXX
|
||||
@@ -1671,6 +1672,7 @@ VAL_ 871 CF_Lvr_IsgState 0 "enabled" 1 "activated" 2 "unknown" 3 "disabled";
|
||||
VAL_ 871 CF_Lvr_Gear 12 "T" 5 "D" 8 "S" 6 "N" 7 "R" 0 "P";
|
||||
VAL_ 882 Elect_Gear_Shifter 4 "S" 5 "D" 8 "S" 6 "N" 7 "R" 0 "P";
|
||||
VAL_ 905 ACCMode 0 "off" 1 "enabled" 2 "driver_override" 3 "off_maybe_fault" 4 "cancelled";
|
||||
VAL_ 905 ObjDistStat 0 "no_object" 1 "receding" 2 "approaching";
|
||||
VAL_ 909 CF_VSM_Warn 2 "FCW" 3 "AEB";
|
||||
VAL_ 916 ACCEnable 0 "SCC ready" 1 "SCC temp fault" 2 "SCC permanent fault" 3 "SCC permanent fault, communication issue";
|
||||
VAL_ 1056 SCCInfoDisplay 0 "No Message" 2 "Cruise Control" 3 "Lost Lead" 4 "Standstill";
|
||||
|
||||
@@ -964,10 +964,14 @@ BO_ 1259 LOCAL_TIME2: 8 XXX
|
||||
SG_ NEW_SIGNAL_3 : 39|1@0+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 1264 LOCAL_TIME: 8 XXX
|
||||
SG_ HOURS : 12|5@0+ (1,0) [0|31] "" XXX
|
||||
SG_ MINUTES : 21|6@0+ (1,0) [0|63] "" XXX
|
||||
SG_ SECONDS : 31|8@0+ (1,0) [0|59] "" XXX
|
||||
SG_ HOURS : 8|8@1+ (1,0) [0|23] "" XXX
|
||||
SG_ MINUTES : 16|8@1+ (1,0) [0|59] "" XXX
|
||||
SG_ SECONDS : 24|8@1+ (1,0) [0|59] "" XXX
|
||||
SG_ MONTH : 34|4@1+ (1,0) [1|12] "" XXX
|
||||
SG_ YEAR : 40|8@1+ (1,2000) [2000|2255] "" XXX
|
||||
SG_ DAY : 48|8@1+ (1,0) [1|31] "" XXX
|
||||
|
||||
CM_ BO_ 1264 "Cluster wall clock, 1Hz. Local time, not UTC. All 0xFF until the cluster initializes.";
|
||||
CM_ SG_ 96 BRAKE_PRESSURE "User applied brake pedal pressure. Ramps from computer applied pressure on falling edge of cruise. Cruise cancels if !=0";
|
||||
CM_ SG_ 101 BRAKE_POSITION "User applied brake pedal position, max is ~700. Signed on some vehicles";
|
||||
CM_ SG_ 203 ADAS_ActvACISta "ADAS Active AngleControlInterface State";
|
||||
|
||||
@@ -1480,6 +1480,7 @@ BO_ 905 SCC14: 8 SCC
|
||||
SG_ JerkLowerLimit : 19|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC
|
||||
SG_ ACCMode : 32|3@1+ (1,0) [0|7] "" CLU,HUD,LDWS_LKAS,ESC
|
||||
SG_ ObjGap : 56|8@1+ (1,0) [0|255] "" CLU,HUD,ESC
|
||||
SG_ ObjDistStat : 42|2@1+ (1,0) [0|3] "" XXX
|
||||
|
||||
BO_ 1157 LFAHDA_MFC: 4 XXX
|
||||
SG_ HDA_USM : 0|2@1+ (1,0) [0|3] "" XXX
|
||||
@@ -1671,6 +1672,7 @@ VAL_ 871 CF_Lvr_IsgState 0 "enabled" 1 "activated" 2 "unknown" 3 "disabled";
|
||||
VAL_ 871 CF_Lvr_Gear 12 "T" 5 "D" 8 "S" 6 "N" 7 "R" 0 "P";
|
||||
VAL_ 882 Elect_Gear_Shifter 4 "S" 5 "D" 8 "S" 6 "N" 7 "R" 0 "P";
|
||||
VAL_ 905 ACCMode 0 "off" 1 "enabled" 2 "driver_override" 3 "off_maybe_fault" 4 "cancelled";
|
||||
VAL_ 905 ObjDistStat 0 "no_object" 1 "receding" 2 "approaching";
|
||||
VAL_ 909 CF_VSM_Warn 2 "FCW" 3 "AEB";
|
||||
VAL_ 916 ACCEnable 0 "SCC ready" 1 "SCC temp fault" 2 "SCC permanent fault" 3 "SCC permanent fault, communication issue";
|
||||
VAL_ 1056 SCCInfoDisplay 0 "No Message" 2 "Cruise Control" 3 "Lost Lead" 4 "Standstill";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
VERSION ""
|
||||
|
||||
NS_ :
|
||||
BS_:
|
||||
BU_: INTERCEPTOR NEO
|
||||
|
||||
BO_ 512 GAS_COMMAND: 6 NEO
|
||||
SG_ GAS_COMMAND : 7|16@0+ (0.672,-177.408) [0|255] "" INTERCEPTOR
|
||||
SG_ GAS_COMMAND2 : 23|16@0+ (0.332,-165.004) [0|255] "" INTERCEPTOR
|
||||
SG_ ENABLE : 39|1@0+ (1,0) [0|1] "" INTERCEPTOR
|
||||
SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" INTERCEPTOR
|
||||
SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" INTERCEPTOR
|
||||
|
||||
BO_ 513 GAS_SENSOR: 6 INTERCEPTOR
|
||||
SG_ INTERCEPTOR_GAS : 7|16@0+ (0.672,-177.408) [0|255] "" NEO
|
||||
SG_ INTERCEPTOR_GAS2 : 23|16@0+ (0.332,-165.004) [0|255] "" NEO
|
||||
SG_ STATE : 39|4@0+ (1,0) [0|15] "" NEO
|
||||
SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" NEO
|
||||
SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" NEO
|
||||
|
||||
VAL_ 513 STATE 5 "FAULT_TIMEOUT" 4 "FAULT_STARTUP" 3 "FAULT_SCE" 2 "FAULT_SEND" 1 "FAULT_BAD_CHECKSUM" 0 "NO_FAULT" ;
|
||||
|
||||
CM_ "Kia Ray EV comma pedal uses the standard 0x200/0x201 rolling counter and CRC8 protocol. Channel scaling is Ray-only, estimated from the September 15 route: sensor rest raw 264/497, slopes about 3.79/7.68 raw counts per native E_EMS11 pedal unit, and 100 native units mapped to 255 comma pedal command units. Confirm against installed Ray firmware before increasing the command cap.";
|
||||
@@ -307,6 +307,16 @@ VAL_ 544 AEB_Status 12 "AEB related" 8 "AEB actuation" 4 "AEB related" 0 "No AEB
|
||||
|
||||
CM_ "subaru_global_2017.dbc starts here";
|
||||
|
||||
BO_ 1723 AVH_Request: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ REQUEST : 16|2@1+ (1,0) [0|3] "" XXX
|
||||
|
||||
BO_ 811 AVH_Status: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ ENABLED : 45|1@1+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 72 Transmission: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
|
||||
@@ -217,6 +217,9 @@ BO_ 513 SDM1: 5 GTW
|
||||
SG_ SDM_bcklPassStatus : 3|2@0+ (1,0) [0|3] "" NEO
|
||||
SG_ SDM_bcklDrivStatus : 5|2@0+ (1,0) [0|3] "" NEO
|
||||
|
||||
BO_ 529 RCM_status: 8 RCM
|
||||
SG_ RCM_buckleDriverStatus : 15|2@0+ (1,0) [0|3] "" GTW,OCS,DAS
|
||||
|
||||
BO_ 532 EPB_epasControl: 3 EPB
|
||||
SG_ EPB_epasControlChecksum : 23|8@0+ (1,0) [0|255] "" NEO,EPAS
|
||||
SG_ EPB_epasControlCounter : 11|4@0+ (1,0) [0|15] "" NEO,EPAS
|
||||
@@ -256,9 +259,11 @@ BO_ 872 DI_state: 8 DI
|
||||
SG_ DI_immobilizerState : 28|3@1+ (1,0) [0|0] "" NEO
|
||||
SG_ DI_speedUnits : 31|1@1+ (1,0) [0|1] "" NEO
|
||||
SG_ DI_cruiseSet : 32|9@1+ (0.5,0) [0|255.5] "speed" NEO
|
||||
SG_ DI_hw1DigitalSpeed : 32|8@1+ (1,0) [0|250] "speed" NEO
|
||||
SG_ DI_aebState : 41|3@1+ (1,0) [0|0] "" NEO
|
||||
SG_ DI_stateCounter : 44|4@1+ (1,0) [0|0] "" NEO
|
||||
SG_ DI_digitalSpeed : 48|8@1+ (1,0) [0|250] "" NEO
|
||||
SG_ DI_hw1CruiseSet : 48|8@1+ (1,0) [0|250] "speed" NEO
|
||||
SG_ DI_stateChecksum : 56|8@1+ (1,0) [0|0] "" NEO
|
||||
|
||||
BO_ 109 SBW_RQ_SCCM: 4 STW
|
||||
@@ -906,4 +911,3 @@ VAL_ 1001 DAS_turnIndicatorRequestReason 6 "DAS_ACTIVE_COMMANDED_LANE_CHANGE" 5
|
||||
VAL_ 1160 DAS_steeringAngleRequest 16384 "ZERO_ANGLE" ;
|
||||
VAL_ 1160 DAS_steeringControlType 1 "ANGLE_CONTROL" 3 "DISABLED" 0 "NONE" 2 "RESERVED" ;
|
||||
VAL_ 1160 DAS_steeringHapticRequest 1 "ACTIVE" 0 "IDLE" ;
|
||||
|
||||
|
||||
@@ -384,3 +384,4 @@ extern const safety_hooks rivian_hooks;
|
||||
extern const safety_hooks psa_hooks;
|
||||
extern const safety_hooks volvo_hooks;
|
||||
extern const safety_hooks tesla_preap_hooks;
|
||||
extern const safety_hooks tesla_legacy_hooks;
|
||||
|
||||
@@ -96,6 +96,8 @@ static bool ford_lka_steering = false;
|
||||
static bool ford_extended_lateral = false;
|
||||
static bool ford_longitudinal = false;
|
||||
static bool ford_cancel_resume_button = false;
|
||||
static bool ford_mach_e_curvature = false;
|
||||
static int ford_path_angle_last = 0;
|
||||
|
||||
// Curvature rate limits
|
||||
#define FORD_LIMITS(limit_lateral_acceleration) { \
|
||||
@@ -121,10 +123,10 @@ static bool ford_cancel_resume_button = false;
|
||||
|
||||
static const AngleSteeringLimits FORD_STEERING_LIMITS = FORD_LIMITS(false);
|
||||
|
||||
#define FORD_EXTENDED_LIMITS(limit_lateral_acceleration) { \
|
||||
#define FORD_EXTENDED_LIMITS(limit_lateral_acceleration, max_curvature_error) { \
|
||||
.max_angle = 1000, \
|
||||
.angle_deg_to_can = 50000, \
|
||||
.max_angle_error = 100, \
|
||||
.max_angle_error = (max_curvature_error), \
|
||||
.angle_rate_up_lookup = { \
|
||||
{5., 16., 25.}, \
|
||||
{0.0025, 0.0014, 0.00018} \
|
||||
@@ -140,7 +142,7 @@ static const AngleSteeringLimits FORD_STEERING_LIMITS = FORD_LIMITS(false);
|
||||
.inactive_angle_is_zero = true, \
|
||||
}
|
||||
|
||||
static const AngleSteeringLimits FORD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(false);
|
||||
static const AngleSteeringLimits FORD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(false, 100);
|
||||
|
||||
static void ford_rx_hook(const CANPacket_t *msg) {
|
||||
if (msg->bus == FORD_MAIN_BUS) {
|
||||
@@ -318,7 +320,8 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
// Safety check for LateralMotionControl2 action
|
||||
if (msg->addr == FORD_LateralMotionControl2) {
|
||||
static const AngleSteeringLimits FORD_CANFD_STEERING_LIMITS = FORD_LIMITS(true);
|
||||
static const AngleSteeringLimits FORD_CANFD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(true);
|
||||
static const AngleSteeringLimits FORD_CANFD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(true, 100);
|
||||
static const AngleSteeringLimits FORD_MACH_E_CURVATURE_LIMITS = FORD_EXTENDED_LIMITS(true, 300);
|
||||
|
||||
// Signal: LatCtl_D2_Rq
|
||||
bool steer_control_enabled = ((msg->data[0] >> 4) & 0x7U) != 0U;
|
||||
@@ -336,9 +339,19 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
if (ford_extended_lateral) {
|
||||
violation |= desired_path_offset != 0;
|
||||
violation |= (desired_curvature_rate < -1024) || (desired_curvature_rate > 1023);
|
||||
violation |= desired_path_angle != 0;
|
||||
if (desired_path_angle != 0) {
|
||||
const float speed = vehicle_speed.max / VEHICLE_SPEED_FACTOR;
|
||||
const float curvature = (float)SAFETY_ABS(desired_curvature) / 50000.0f;
|
||||
const float path_angle = (float)SAFETY_ABS(desired_path_angle) / 2000.0f;
|
||||
const float combined_acceleration = (curvature + path_angle / SAFETY_MAX(speed, 1.0f)) * speed * speed;
|
||||
violation |= !ford_mach_e_curvature || !steer_control_enabled || !controls_allowed;
|
||||
violation |= (speed < 3.0f) || (speed >= 8.8f);
|
||||
violation |= (SAFETY_ABS(desired_curvature) < 975) || (SAFETY_ABS(desired_path_angle) > 320);
|
||||
violation |= (desired_curvature * desired_path_angle <= 0) || (combined_acceleration > 2.5f);
|
||||
violation |= SAFETY_ABS(desired_path_angle - ford_path_angle_last) > 110;
|
||||
}
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled,
|
||||
FORD_CANFD_EXTENDED_STEERING_LIMITS);
|
||||
ford_mach_e_curvature ? FORD_MACH_E_CURVATURE_LIMITS : FORD_CANFD_EXTENDED_STEERING_LIMITS);
|
||||
if (!steer_control_enabled) {
|
||||
violation |= (desired_curvature != 0) || (desired_curvature_rate != 0);
|
||||
}
|
||||
@@ -352,6 +365,8 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
|
||||
if (violation) {
|
||||
tx = false;
|
||||
} else {
|
||||
ford_path_angle_last = desired_path_angle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,8 +421,11 @@ static safety_config ford_init(uint16_t param) {
|
||||
|
||||
const uint16_t FORD_PARAM_CANFD = 2;
|
||||
const uint16_t FORD_PARAM_LKA_STEERING = 4;
|
||||
const uint16_t FORD_PARAM_MACH_E_CURVATURE = 8;
|
||||
const bool ford_canfd = GET_FLAG(param, FORD_PARAM_CANFD);
|
||||
ford_lka_steering = GET_FLAG(param, FORD_PARAM_LKA_STEERING);
|
||||
ford_mach_e_curvature = ford_canfd && GET_FLAG(param, FORD_PARAM_MACH_E_CURVATURE);
|
||||
ford_path_angle_last = 0;
|
||||
ford_extended_lateral = false;
|
||||
ford_cancel_resume_button = false;
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ const LongitudinalLimits HYUNDAI_LONG_LIMITS = {
|
||||
#define HYUNDAI_NON_SCC_EV_ADDR_CHECK \
|
||||
{.msg = {{0x592U, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \
|
||||
|
||||
#define HYUNDAI_RAY_PEDAL_ADDR_CHECK \
|
||||
{.msg = {{0x201U, 0, 6, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \
|
||||
|
||||
static const CanMsg HYUNDAI_TX_MSGS[] = {
|
||||
HYUNDAI_COMMON_TX_MSGS(0, false)
|
||||
};
|
||||
@@ -75,6 +78,11 @@ static const CanMsg HYUNDAI_REFRESH_TX_MSGS[] = {
|
||||
HYUNDAI_COMMON_TX_MSGS(0, true)
|
||||
};
|
||||
|
||||
static const CanMsg HYUNDAI_RAY_PEDAL_TX_MSGS[] = {
|
||||
HYUNDAI_COMMON_TX_MSGS(0, true)
|
||||
{0x200, 0, 6, .check_relay = false}, // comma pedal only, not Hyundai EMS20
|
||||
};
|
||||
|
||||
static const CanMsg HYUNDAI_LONG_TX_MSGS[] = {
|
||||
HYUNDAI_LONG_COMMON_TX_MSGS(0, false)
|
||||
{0x38D, 0, 8, .check_relay = false}, // FCA11 Bus 0
|
||||
@@ -90,6 +98,7 @@ static const CanMsg HYUNDAI_LONG_REFRESH_TX_MSGS[] = {
|
||||
};
|
||||
|
||||
static bool hyundai_legacy = false;
|
||||
static bool hyundai_ray_pedal = false;
|
||||
static bool hyundai_can_canfd_blended_hda2 = false;
|
||||
static bool hyundai_acc_main_on_rx_prev = false;
|
||||
|
||||
@@ -120,6 +129,8 @@ static uint8_t hyundai_get_counter(const CANPacket_t *msg) {
|
||||
cnt = byte_421 & 0xFU;
|
||||
} else if (msg->addr == 0x4F1U) {
|
||||
cnt = (msg->data[3] >> 4) & 0xFU;
|
||||
} else if (hyundai_ray_pedal && (msg->addr == 0x201U)) {
|
||||
cnt = msg->data[4] & 0xFU;
|
||||
} else {
|
||||
}
|
||||
return cnt;
|
||||
@@ -136,11 +147,24 @@ static uint32_t hyundai_get_checksum(const CANPacket_t *msg) {
|
||||
chksum = msg->data[6] & 0xFU;
|
||||
} else if (msg->addr == 0x421U) {
|
||||
chksum = hyundai_can_canfd_blended ? msg->data[0] : msg->data[7] >> 4;
|
||||
} else if (hyundai_ray_pedal && (msg->addr == 0x201U)) {
|
||||
chksum = msg->data[5];
|
||||
} else {
|
||||
}
|
||||
return chksum;
|
||||
}
|
||||
|
||||
static uint8_t hyundai_ray_pedal_checksum(const CANPacket_t *msg) {
|
||||
uint8_t crc = 0xFFU;
|
||||
for (int i = 4; i >= 0; i--) {
|
||||
crc ^= msg->data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
crc = (crc & 0x80U) ? (uint8_t)((crc << 1U) ^ 0xD5U) : (uint8_t)(crc << 1U);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static void hyundai_rx_all_hook(const CANPacket_t *msg) {
|
||||
if ((msg->addr == 0x53EU) && (msg->bus == 2U) && (GET_LEN(msg) == 6U)) {
|
||||
hyundai_has_lkas12 = true;
|
||||
@@ -148,6 +172,10 @@ static void hyundai_rx_all_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static uint32_t hyundai_compute_checksum(const CANPacket_t *msg) {
|
||||
if (hyundai_ray_pedal && (msg->addr == 0x201U)) {
|
||||
return hyundai_ray_pedal_checksum(msg);
|
||||
}
|
||||
|
||||
uint8_t chksum = 0;
|
||||
if (msg->addr == 0x386U) {
|
||||
// count the bits
|
||||
@@ -231,7 +259,11 @@ static void hyundai_rx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
// gas press, different for EV, hybrid, and ICE models
|
||||
if ((msg->addr == 0x371U) && hyundai_ev_gas_signal) {
|
||||
if ((msg->addr == 0x201U) && hyundai_ray_pedal) {
|
||||
const uint16_t track1 = ((uint16_t)msg->data[0] << 8U) | msg->data[1];
|
||||
const uint16_t track2 = ((uint16_t)msg->data[2] << 8U) | msg->data[3];
|
||||
gas_pressed = (track1 > 272U) || (track2 > 513U);
|
||||
} else if ((msg->addr == 0x371U) && hyundai_ev_gas_signal && !hyundai_ray_pedal) {
|
||||
gas_pressed = (((msg->data[4] & 0x7FU) << 1) | (msg->data[3] >> 7)) != 0U;
|
||||
} else if ((msg->addr == 0x371U) && hyundai_hybrid_gas_signal) {
|
||||
gas_pressed = msg->data[7] != 0U;
|
||||
@@ -291,6 +323,22 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
|
||||
|
||||
bool tx = true;
|
||||
|
||||
if (hyundai_ray_pedal && (msg->addr == 0x200U)) {
|
||||
const uint16_t track1 = ((uint16_t)msg->data[0] << 8U) | msg->data[1];
|
||||
const uint16_t track2 = ((uint16_t)msg->data[2] << 8U) | msg->data[3];
|
||||
const bool enabled = (msg->data[4] & 0x80U) != 0U;
|
||||
const int expected_track2 = 497 + (2 * ((int)track1 - 264));
|
||||
if ((msg->data[4] & 0x70U) != 0U ||
|
||||
(msg->data[5] != hyundai_ray_pedal_checksum(msg)) ||
|
||||
(enabled && (track1 < 264U || track1 > 473U || track2 < 497U || track2 > 919U ||
|
||||
SAFETY_ABS((int)track2 - expected_track2) > 40)) ||
|
||||
(!enabled && ((track1 != 0U) || (track2 != 0U))) ||
|
||||
longitudinal_interceptor_checks(msg) ||
|
||||
(enabled && (!get_longitudinal_allowed() || brake_pressed_prev))) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((msg->addr == 0x53EU) && !hyundai_has_lkas12) {
|
||||
tx = false;
|
||||
}
|
||||
@@ -390,6 +438,10 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
|
||||
return tx;
|
||||
}
|
||||
|
||||
static bool hyundai_fwd_hook(int bus_num, int addr) {
|
||||
return (bus_num == 2) && (addr == 0x53E) && hyundai_has_lkas12;
|
||||
}
|
||||
|
||||
static safety_config hyundai_init(uint16_t param) {
|
||||
static const CanMsg HYUNDAI_CAMERA_SCC_TX_MSGS[] = {
|
||||
HYUNDAI_COMMON_TX_MSGS(2, false)
|
||||
@@ -457,6 +509,10 @@ static safety_config hyundai_init(uint16_t param) {
|
||||
};
|
||||
|
||||
hyundai_common_init(param);
|
||||
hyundai_ray_pedal = (param & (uint16_t)~(32U | 128U | 2048U)) == 0x9405U;
|
||||
if (hyundai_ray_pedal) {
|
||||
hyundai_longitudinal = true; // button engagement; no Hyundai SCC TX
|
||||
}
|
||||
hyundai_legacy = false;
|
||||
hyundai_can_canfd_blended_hda2 = hyundai_can_canfd_blended && hyundai_canfd_lka_steering;
|
||||
hyundai_aol_main_lkas_sync = GET_FLAG(param, 32U);
|
||||
@@ -467,6 +523,17 @@ static safety_config hyundai_init(uint16_t param) {
|
||||
}
|
||||
|
||||
safety_config ret;
|
||||
if (hyundai_ray_pedal) {
|
||||
static RxCheck hyundai_ray_pedal_rx_checks[] = {
|
||||
HYUNDAI_COMMON_RX_CHECKS(false)
|
||||
HYUNDAI_NON_SCC_EV_ADDR_CHECK
|
||||
HYUNDAI_LDA_BUTTON_ADDR_CHECK
|
||||
HYUNDAI_RAY_PEDAL_ADDR_CHECK
|
||||
};
|
||||
SET_RX_CHECKS(hyundai_ray_pedal_rx_checks, ret);
|
||||
SET_TX_MSGS(HYUNDAI_RAY_PEDAL_TX_MSGS, ret);
|
||||
return ret;
|
||||
}
|
||||
if (hyundai_longitudinal) {
|
||||
// Use CLU11 (buttons) to manage controls allowed instead of SCC cruise state
|
||||
static RxCheck hyundai_long_rx_checks[] = {
|
||||
@@ -696,6 +763,7 @@ static safety_config hyundai_legacy_init(uint16_t param) {
|
||||
|
||||
hyundai_common_init(param);
|
||||
hyundai_legacy = true;
|
||||
hyundai_ray_pedal = false;
|
||||
hyundai_can_canfd_blended_hda2 = false;
|
||||
hyundai_camera_scc = false;
|
||||
hyundai_can_refresh_msgs = false;
|
||||
@@ -711,6 +779,7 @@ const safety_hooks hyundai_hooks = {
|
||||
.get_counter = hyundai_get_counter,
|
||||
.get_checksum = hyundai_get_checksum,
|
||||
.compute_checksum = hyundai_compute_checksum,
|
||||
.fwd = hyundai_fwd_hook,
|
||||
};
|
||||
|
||||
const safety_hooks hyundai_legacy_hooks = {
|
||||
@@ -721,4 +790,5 @@ const safety_hooks hyundai_legacy_hooks = {
|
||||
.get_counter = hyundai_get_counter,
|
||||
.get_checksum = hyundai_get_checksum,
|
||||
.compute_checksum = hyundai_compute_checksum,
|
||||
.fwd = hyundai_fwd_hook,
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
static bool hyundai_canfd_alt_buttons = false;
|
||||
static bool hyundai_canfd_lka_steering_alt = false;
|
||||
static bool hyundai_canfd_angle_steering = false;
|
||||
static bool hyundai_canfd_no_stock_lka = false;
|
||||
static bool hyundai_ccnc = false;
|
||||
static bool hyundai_canfd_ccnc_angle_long = false;
|
||||
static bool hyundai_canfd_lka_alt_drive_gear = false;
|
||||
@@ -100,7 +101,8 @@ static bool hyundai_canfd_lka_alt_openpilot_allowed(void) {
|
||||
}
|
||||
|
||||
static bool hyundai_canfd_lka_alt_stock_forwarding(void) {
|
||||
return hyundai_canfd_lka_steering_alt && hyundai_canfd_angle_steering && !hyundai_canfd_lka_alt_openpilot_allowed();
|
||||
return hyundai_canfd_lka_steering_alt && hyundai_canfd_angle_steering &&
|
||||
!hyundai_canfd_no_stock_lka && !hyundai_canfd_lka_alt_openpilot_allowed();
|
||||
}
|
||||
|
||||
static void hyundai_canfd_rx_all_hook(const CANPacket_t *msg) {
|
||||
@@ -270,6 +272,10 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) {
|
||||
const int lkas_angle_active = (msg->data[9] >> 4U) & 0x3U;
|
||||
const bool steer_angle_req = lkas_angle_active != 1;
|
||||
|
||||
if (hyundai_canfd_no_stock_lka && steer_angle_req && !hyundai_canfd_lka_alt_openpilot_allowed()) {
|
||||
tx = false;
|
||||
}
|
||||
|
||||
int desired_angle = (msg->data[11] << 6U) | (msg->data[10] >> 2U);
|
||||
desired_angle = to_signed(desired_angle, 14);
|
||||
|
||||
@@ -364,6 +370,7 @@ static safety_config hyundai_canfd_init(uint16_t param) {
|
||||
const uint16_t HYUNDAI_PARAM_CANFD_LKA_STEERING_ALT = 128;
|
||||
const uint16_t HYUNDAI_PARAM_CANFD_ALT_BUTTONS = 32;
|
||||
const uint16_t HYUNDAI_PARAM_CANFD_ANGLE_STEERING = 1024;
|
||||
const uint16_t HYUNDAI_PARAM_CANFD_NO_STOCK_LKA = 4096U;
|
||||
const uint16_t HYUNDAI_PARAM_CCNC = 32768U;
|
||||
|
||||
static const CanMsg HYUNDAI_CANFD_LKA_STEERING_TX_MSGS[] = {
|
||||
@@ -479,12 +486,15 @@ static safety_config hyundai_canfd_init(uint16_t param) {
|
||||
{0x7C4, 2, 8, .check_relay = true}, /* camera support frame */ \
|
||||
{0xEA, 2, 24, .check_relay = true}, /* MDPS support frame */ \
|
||||
|
||||
hyundai_common_init(param);
|
||||
// This CAN-FD-only bit is independent of classic CAN's NON_SCC mode.
|
||||
hyundai_common_init(param & ~HYUNDAI_PARAM_CANFD_NO_STOCK_LKA);
|
||||
|
||||
gen_crc_lookup_table_16(0x1021, hyundai_canfd_crc_lut);
|
||||
hyundai_canfd_alt_buttons = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ALT_BUTTONS);
|
||||
hyundai_canfd_lka_steering_alt = GET_FLAG(param, HYUNDAI_PARAM_CANFD_LKA_STEERING_ALT);
|
||||
hyundai_canfd_angle_steering = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE_STEERING);
|
||||
hyundai_canfd_no_stock_lka = hyundai_canfd_angle_steering && hyundai_canfd_lka_steering &&
|
||||
hyundai_canfd_lka_steering_alt && GET_FLAG(param, HYUNDAI_PARAM_CANFD_NO_STOCK_LKA);
|
||||
hyundai_ccnc = GET_FLAG(param, HYUNDAI_PARAM_CCNC);
|
||||
hyundai_canfd_ccnc_angle_long = hyundai_longitudinal && hyundai_canfd_lka_steering &&
|
||||
hyundai_canfd_lka_steering_alt && hyundai_canfd_angle_steering && hyundai_ccnc;
|
||||
|
||||
@@ -136,6 +136,8 @@ static uint32_t subaru_compute_checksum(const CANPacket_t *msg) {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
#include "opendbc/safety/modes/subaru_avh.h"
|
||||
|
||||
static void subaru_rx_hook(const CANPacket_t *msg) {
|
||||
const unsigned int alt_main_bus = subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_MAIN_BUS;
|
||||
const unsigned int status_bus = subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_CAM_BUS;
|
||||
@@ -308,6 +310,10 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
|
||||
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
|
||||
}
|
||||
|
||||
if (msg->addr == 0x6BBU) {
|
||||
violation |= !subaru_avh_tx(msg);
|
||||
}
|
||||
|
||||
if (violation){
|
||||
tx = false;
|
||||
}
|
||||
@@ -315,6 +321,12 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config subaru_init(uint16_t param) {
|
||||
static const CanMsg SUBARU_LEGACY_AVH_TX_MSGS[] = {
|
||||
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
|
||||
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
|
||||
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
|
||||
{0x6BBU, SUBARU_ALT_BUS, 8, .check_relay = false},
|
||||
};
|
||||
static const CanMsg SUBARU_TX_MSGS[] = {
|
||||
SUBARU_BASE_TX_MSGS(SUBARU_MAIN_BUS, MSG_SUBARU_ES_LKAS)
|
||||
SUBARU_COMMON_TX_MSGS(SUBARU_MAIN_BUS)
|
||||
@@ -455,12 +467,22 @@ static safety_config subaru_init(uint16_t param) {
|
||||
subaru_stop_and_go ? BUILD_SAFETY_CFG(subaru_rx_checks, SUBARU_STOP_AND_GO_TX_MSGS) : \
|
||||
BUILD_SAFETY_CFG(subaru_rx_checks, SUBARU_TX_MSGS);
|
||||
}
|
||||
bool avh_enabled = false;
|
||||
#ifdef ALLOW_DEBUG
|
||||
avh_enabled = GET_FLAG(param, 1024U) && subaru_gen2 && subaru_lkas_angle && subaru_fixed_angle_limits &&
|
||||
subaru_stop_start_button && !subaru_d_platform && !GET_FLAG(param, 2U) && !subaru_redneck_cruise;
|
||||
#endif
|
||||
subaru_avh_init(avh_enabled);
|
||||
if (avh_enabled) {
|
||||
ret = BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_LEGACY_AVH_TX_MSGS);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const safety_hooks subaru_hooks = {
|
||||
.init = subaru_init,
|
||||
.rx = subaru_rx_hook,
|
||||
.rx_all = subaru_avh_rx,
|
||||
.tx = subaru_tx_hook,
|
||||
.get_counter = subaru_get_counter,
|
||||
.get_checksum = subaru_get_checksum,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
// Legacy startup AVH only. Never transmit the 0x32B status message.
|
||||
static const unsigned int SUBARU_AVH_INPUTS[] = {0x6BBU, 0x32BU, 0x40U, 0x48U, 0x13AU, 0x174U};
|
||||
static uint8_t subaru_avh_data[6][8];
|
||||
static uint32_t subaru_avh_ts[6];
|
||||
static bool subaru_avh_seen[6];
|
||||
static bool subaru_avh_seq[6];
|
||||
static bool subaru_avh_enabled;
|
||||
static bool subaru_avh_done;
|
||||
static unsigned int subaru_avh_count;
|
||||
static uint32_t subaru_avh_start;
|
||||
static uint32_t subaru_avh_sent;
|
||||
static uint32_t subaru_avh_template_ts;
|
||||
static uint32_t subaru_avh_stable_since;
|
||||
static bool subaru_avh_stable;
|
||||
|
||||
static void subaru_avh_init(bool enabled) {
|
||||
subaru_avh_enabled = enabled;
|
||||
subaru_avh_done = false;
|
||||
subaru_avh_count = 0U;
|
||||
subaru_avh_start = microsecond_timer_get();
|
||||
subaru_avh_sent = 0U;
|
||||
subaru_avh_template_ts = 0U;
|
||||
subaru_avh_stable_since = 0U;
|
||||
subaru_avh_stable = false;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
subaru_avh_seen[i] = false;
|
||||
subaru_avh_seq[i] = false;
|
||||
subaru_avh_ts[i] = 0U;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
subaru_avh_data[i][j] = 0U;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool subaru_avh_ready(uint32_t now) {
|
||||
bool ready = true;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
ready &= subaru_avh_seen[i] && subaru_avh_seq[i] &&
|
||||
(safety_get_ts_elapsed(now, subaru_avh_ts[i]) <= ((i == 0) ? 1500000U : 300000U));
|
||||
}
|
||||
const unsigned int rpm = ((unsigned int)subaru_avh_data[2][2] | ((unsigned int)subaru_avh_data[2][3] << 8U)) & 0x1FFFU;
|
||||
ready &= (rpm >= 400U) && (subaru_avh_data[2][4] == 0U) && (subaru_avh_data[3][3] == 4U);
|
||||
ready &= (subaru_avh_data[5][2] & 8U) != 0U;
|
||||
ready &= !vehicle_moving && !controls_allowed;
|
||||
return ready;
|
||||
}
|
||||
|
||||
static void subaru_avh_rx(const CANPacket_t *msg) {
|
||||
if (subaru_avh_enabled && !subaru_avh_done && (msg->bus == 1U)) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (msg->addr == SUBARU_AVH_INPUTS[i]) {
|
||||
if ((GET_LEN(msg) != 8U) || (subaru_get_checksum(msg) != subaru_compute_checksum(msg))) {
|
||||
subaru_avh_done = true;
|
||||
} else {
|
||||
const uint8_t old_counter = subaru_avh_data[i][1] & 0xFU;
|
||||
const uint8_t counter = msg->data[1] & 0xFU;
|
||||
if (!subaru_avh_seen[i] || (counter != old_counter)) {
|
||||
subaru_avh_seq[i] = subaru_avh_seen[i] && (counter == ((old_counter + 1U) & 0xFU));
|
||||
subaru_avh_seen[i] = true;
|
||||
subaru_avh_ts[i] = now;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
subaru_avh_data[i][j] = msg->data[j];
|
||||
}
|
||||
}
|
||||
if (((i == 0) && ((msg->data[2] & 3U) != 0U)) ||
|
||||
((i == 1) && ((msg->data[5] & 0x20U) != 0U)) ||
|
||||
((i == 2) && (msg->data[4] != 0U)) || ((i == 3) && (msg->data[3] != 4U)) ||
|
||||
((i == 4) && (((GET_BYTES(msg, 1, 3) >> 4) & 0x1FFFU) != 0U ||
|
||||
((GET_BYTES(msg, 3, 3) >> 1) & 0x1FFFU) != 0U ||
|
||||
((GET_BYTES(msg, 4, 3) >> 6) & 0x1FFFU) != 0U ||
|
||||
((GET_BYTES(msg, 6, 2) >> 3) & 0x1FFFU) != 0U))) {
|
||||
subaru_avh_done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (controls_allowed || (safety_get_ts_elapsed(now, subaru_avh_start) > 30000000U)) {
|
||||
subaru_avh_done = true;
|
||||
}
|
||||
if (!subaru_avh_ready(now)) {
|
||||
subaru_avh_stable = false;
|
||||
if (subaru_avh_count > 0U) {
|
||||
subaru_avh_done = true;
|
||||
}
|
||||
} else if (!subaru_avh_stable) {
|
||||
subaru_avh_stable = true;
|
||||
subaru_avh_stable_since = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool subaru_avh_tx(const CANPacket_t *msg) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const uint32_t elapsed = safety_get_ts_elapsed(now, subaru_avh_start);
|
||||
const bool second = subaru_avh_count == 1U;
|
||||
bool allowed = subaru_avh_enabled && !subaru_avh_done && (subaru_avh_count < 2U) &&
|
||||
(msg->bus == 1U) && (GET_LEN(msg) == 8U) && !safety_rx_checks_invalid &&
|
||||
(elapsed >= 10000000U) && (elapsed <= 30000000U) && subaru_avh_ready(now) &&
|
||||
subaru_avh_stable && (safety_get_ts_elapsed(now, subaru_avh_stable_since) >= 3000000U);
|
||||
// Rejected generic RX frames may not reach our hook; invalidate their cached inputs too.
|
||||
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||
const RxCheck *check = ¤t_safety_config.rx_checks[i];
|
||||
for (int j = 0; j < 6; j++) {
|
||||
if (((unsigned int)check->msg[check->status.index].addr == SUBARU_AVH_INPUTS[j]) && (check->msg[check->status.index].bus == 1U)) {
|
||||
allowed &= check->status.valid_checksum && (check->status.wrong_counters < MAX_WRONG_COUNTERS);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (second) {
|
||||
const uint32_t spacing = safety_get_ts_elapsed(now, subaru_avh_sent);
|
||||
allowed &= (spacing >= 45000U) && (spacing <= 80000U) && (subaru_avh_ts[0] == subaru_avh_template_ts) &&
|
||||
(safety_get_ts_elapsed(now, subaru_avh_ts[0]) <= 110000U);
|
||||
} else {
|
||||
allowed &= safety_get_ts_elapsed(now, subaru_avh_ts[0]) <= 30000U;
|
||||
}
|
||||
uint8_t sum = (uint8_t)(0xBBU + 6U);
|
||||
for (int i = 1; i < 8; i++) {
|
||||
uint8_t expected = subaru_avh_data[0][i];
|
||||
if (i == 1) {
|
||||
expected = (expected & 0xF0U) | ((expected + (second ? 2U : 1U)) & 0xFU);
|
||||
} else if (i == 2) {
|
||||
expected |= 2U;
|
||||
} else {
|
||||
// Preserve every unrelated payload bit.
|
||||
}
|
||||
allowed &= msg->data[i] == expected;
|
||||
sum += expected;
|
||||
}
|
||||
allowed &= msg->data[0] == sum;
|
||||
if (allowed) {
|
||||
subaru_avh_count++;
|
||||
subaru_avh_sent = now;
|
||||
subaru_avh_template_ts = subaru_avh_ts[0];
|
||||
subaru_avh_done = second;
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#pragma once
|
||||
|
||||
#include "opendbc/safety/declarations.h"
|
||||
|
||||
#define TESLA_LEGACY_FLAG_HW1 8U
|
||||
|
||||
static bool tesla_external_panda = false;
|
||||
static bool tesla_hw1 = false;
|
||||
static bool tesla_hw2 = false;
|
||||
static bool tesla_hw3 = false;
|
||||
static bool tesla_legacy_longitudinal = false;
|
||||
|
||||
static int chassis_bus = 0U;
|
||||
static int das_control_msg = 0x2bfU;
|
||||
static int di_torque1_msg = 0x106U;
|
||||
|
||||
static bool tesla_legacy_stock_aeb = false;
|
||||
static bool tesla_legacy_stock_lkas = false;
|
||||
static bool tesla_legacy_stock_lkas_prev = false;
|
||||
|
||||
static void tesla_legacy_rx_hook(const CANPacket_t *msg) {
|
||||
// EPAS_sysStatus: steering angle, driver hands, and EAC status.
|
||||
if (!tesla_external_panda && (msg->bus == 0U) && (msg->addr == 0x370U)) {
|
||||
const int angle_meas_new = (((msg->data[4] & 0x3FU) << 8) | msg->data[5]) - 8192U;
|
||||
update_sample(&angle_meas, angle_meas_new);
|
||||
|
||||
const int hands_on_level = msg->data[4] >> 6;
|
||||
const int eac_status = msg->data[6] >> 5;
|
||||
const int eac_error_code = msg->data[2] >> 4;
|
||||
steering_disengage = (hands_on_level >= 3) || ((eac_status == 0) && (eac_error_code == 9));
|
||||
}
|
||||
|
||||
// ESP_B: ESP_vehicleSpeed.
|
||||
if (!tesla_external_panda && (msg->bus == (unsigned)chassis_bus) && (msg->addr == 0x155U)) {
|
||||
const float speed = ((msg->data[6] | (msg->data[5] << 8)) * 0.01) * KPH_TO_MS;
|
||||
UPDATE_VEHICLE_SPEED(speed);
|
||||
}
|
||||
|
||||
// DI_torque1: pedal position. HW1 uses the 0x108 message variant.
|
||||
if ((tesla_external_panda || tesla_hw1) && (msg->bus == 0U) && (msg->addr == di_torque1_msg)) {
|
||||
gas_pressed = msg->data[6] != 0U;
|
||||
}
|
||||
|
||||
if (((tesla_external_panda) && (msg->bus == 0U) && (msg->addr == 0x1f8U)) ||
|
||||
((!tesla_external_panda) && (msg->bus == (unsigned)chassis_bus) && (msg->addr == 0x20aU))) {
|
||||
brake_pressed = (((msg->data[0] & 0x0CU) >> 2) != 1U);
|
||||
}
|
||||
|
||||
// DI_state: cruise state.
|
||||
if (((tesla_external_panda) && (msg->bus == 0U) && (msg->addr == 0x256U)) ||
|
||||
((!tesla_external_panda) && (msg->bus == (unsigned)chassis_bus) && (msg->addr == 0x368U))) {
|
||||
const int cruise_state = (msg->data[1] >> 4) & 0x07U;
|
||||
const bool cruise_engaged = (cruise_state == 2) || (cruise_state == 3) || (cruise_state == 4) ||
|
||||
(cruise_state == 6) || (cruise_state == 7);
|
||||
vehicle_moving = cruise_state != 3;
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
}
|
||||
|
||||
if (msg->bus == 2U) {
|
||||
if ((tesla_external_panda || tesla_hw1) && msg->addr == das_control_msg) {
|
||||
tesla_legacy_stock_aeb = (msg->data[2] & 0x03U) == 1U;
|
||||
}
|
||||
|
||||
if (!tesla_external_panda && msg->addr == 0x488U) {
|
||||
const int steering_control_type = msg->data[2] >> 6;
|
||||
const bool stock_lkas_now = steering_control_type == 2;
|
||||
if (stock_lkas_now && !tesla_legacy_stock_lkas_prev && !controls_allowed) {
|
||||
tesla_legacy_stock_lkas = true;
|
||||
}
|
||||
if (!stock_lkas_now) {
|
||||
tesla_legacy_stock_lkas = false;
|
||||
}
|
||||
tesla_legacy_stock_lkas_prev = stock_lkas_now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool tesla_legacy_tx_hook(const CANPacket_t *msg) {
|
||||
const AngleSteeringLimits TESLA_STEERING_LIMITS = {
|
||||
.max_angle = 3600,
|
||||
.angle_deg_to_can = 10,
|
||||
.frequency = 50U,
|
||||
};
|
||||
|
||||
const AngleSteeringParams TESLA_LEGACY_STEERING_PARAMS = {
|
||||
.slip_factor = -0.0005666493436310427,
|
||||
.steer_ratio = 15.,
|
||||
.wheelbase = 2.96,
|
||||
};
|
||||
|
||||
const LongitudinalLimits TESLA_LONG_LIMITS = {
|
||||
.max_accel = 425,
|
||||
.min_accel = 288,
|
||||
.inactive_accel = 375,
|
||||
};
|
||||
|
||||
bool violation = false;
|
||||
|
||||
// DAS_steeringControl: angle is encoded in 0.1 degree units with a 1638.35 offset.
|
||||
if (!tesla_external_panda && (msg->addr == 0x488U)) {
|
||||
const int raw_angle_can = ((msg->data[0] & 0x7FU) << 8) | msg->data[1];
|
||||
const int desired_angle = raw_angle_can - 16384;
|
||||
const int steer_control_type = msg->data[2] >> 6;
|
||||
const bool steer_control_enabled = steer_control_type == 1;
|
||||
|
||||
violation |= steer_angle_cmd_checks_vm(desired_angle, steer_control_enabled,
|
||||
TESLA_STEERING_LIMITS, TESLA_LEGACY_STEERING_PARAMS);
|
||||
|
||||
const bool valid_steer_control_type = (steer_control_type == 0) || (steer_control_type == 1);
|
||||
violation |= !valid_steer_control_type;
|
||||
violation |= tesla_legacy_stock_lkas;
|
||||
}
|
||||
|
||||
// DAS_control: HW1 longitudinal control is sent to the powertrain bus (bus 0).
|
||||
if ((tesla_external_panda || tesla_hw1) && (msg->addr == das_control_msg)) {
|
||||
const int aeb_event = msg->data[2] & 0x03U;
|
||||
violation |= aeb_event != 0;
|
||||
violation |= tesla_legacy_stock_aeb;
|
||||
|
||||
const int raw_accel_max = ((msg->data[6] & 0x1FU) << 4) | (msg->data[5] >> 4);
|
||||
const int raw_accel_min = ((msg->data[5] & 0x0FU) << 5) | (msg->data[4] >> 3);
|
||||
if (tesla_legacy_longitudinal) {
|
||||
violation |= (raw_accel_max < TESLA_LONG_LIMITS.inactive_accel) &&
|
||||
(raw_accel_min < TESLA_LONG_LIMITS.inactive_accel);
|
||||
violation |= longitudinal_accel_checks(raw_accel_max, TESLA_LONG_LIMITS);
|
||||
violation |= longitudinal_accel_checks(raw_accel_min, TESLA_LONG_LIMITS);
|
||||
} else {
|
||||
// Stock ACC may only be cancelled, never spoofed or accelerated.
|
||||
const int acc_state = msg->data[1] >> 4;
|
||||
violation |= acc_state != 13;
|
||||
violation |= (raw_accel_max != TESLA_LONG_LIMITS.inactive_accel) ||
|
||||
(raw_accel_min != TESLA_LONG_LIMITS.inactive_accel);
|
||||
}
|
||||
}
|
||||
|
||||
return !violation;
|
||||
}
|
||||
|
||||
static bool tesla_legacy_fwd_hook(int bus_num, int addr) {
|
||||
bool block_msg = false;
|
||||
|
||||
if (bus_num == 2) {
|
||||
if (!tesla_external_panda && !tesla_hw1 && (addr == 0x27dU)) {
|
||||
block_msg = true;
|
||||
}
|
||||
if (!tesla_external_panda && (addr == 0x488U) && !tesla_legacy_stock_lkas) {
|
||||
block_msg = true;
|
||||
}
|
||||
if ((tesla_external_panda || tesla_hw1) && (addr == das_control_msg) && !tesla_legacy_stock_aeb) {
|
||||
block_msg = true;
|
||||
}
|
||||
}
|
||||
|
||||
return block_msg;
|
||||
}
|
||||
|
||||
static safety_config tesla_legacy_init(uint16_t param) {
|
||||
const int TESLA_FLAG_EXTERNAL_PANDA = 4;
|
||||
const int TESLA_FLAG_HW2 = 16;
|
||||
const int TESLA_FLAG_HW3 = 32;
|
||||
|
||||
tesla_external_panda = GET_FLAG(param, TESLA_FLAG_EXTERNAL_PANDA);
|
||||
tesla_hw1 = GET_FLAG(param, TESLA_LEGACY_FLAG_HW1);
|
||||
tesla_hw2 = GET_FLAG(param, TESLA_FLAG_HW2);
|
||||
tesla_hw3 = GET_FLAG(param, TESLA_FLAG_HW3);
|
||||
tesla_legacy_longitudinal = GET_FLAG(param, 1);
|
||||
|
||||
tesla_legacy_stock_aeb = false;
|
||||
tesla_legacy_stock_lkas = false;
|
||||
tesla_legacy_stock_lkas_prev = false;
|
||||
chassis_bus = 0U;
|
||||
di_torque1_msg = 0x106U;
|
||||
das_control_msg = tesla_external_panda ? 0x2bfU : 0x2b9U;
|
||||
|
||||
static const CanMsg TESLA_TX_LEGACY_MSGS[] = {
|
||||
{0x488, 0, 4, .check_relay = true, .disable_static_blocking = true},
|
||||
{0x27D, 0, 3, .check_relay = true, .disable_static_blocking = true},
|
||||
};
|
||||
|
||||
static const CanMsg TESLA_LEGACY_PT_MSGS[] = {
|
||||
{0x2bf, 0, 8, .check_relay = true, .disable_static_blocking = true},
|
||||
};
|
||||
|
||||
static const CanMsg TESLA_TX_LEGACY_HW1_MSGS[] = {
|
||||
{0x488, 0, 4, .check_relay = true, .disable_static_blocking = true},
|
||||
{0x2b9, 0, 8, .check_relay = true, .disable_static_blocking = true},
|
||||
};
|
||||
|
||||
static RxCheck tesla_legacy_pt_rx_checks[] = {
|
||||
{.msg = {{0x106, 0, 8, 100U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x1f8, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x2bf, 2, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x256, 0, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
};
|
||||
|
||||
static RxCheck tesla_legacy_hw1_rx_checks[] = {
|
||||
{.msg = {{0x108, 0, 8, 100U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x2b9, 2, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x370, 0, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x155, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x20a, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x368, 0, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x488, 2, 4, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
};
|
||||
|
||||
static RxCheck tesla_legacy_hw2_rx_checks[] = {
|
||||
{.msg = {{0x370, 0, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x155, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x20a, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x368, 0, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x488, 2, 4, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
};
|
||||
|
||||
static RxCheck tesla_legacy_hw3_rx_checks[] = {
|
||||
{.msg = {{0x370, 0, 8, 100U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x155, 1, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x20a, 1, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x368, 1, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
{.msg = {{0x488, 2, 4, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, {0}, {0}}},
|
||||
};
|
||||
|
||||
if (tesla_external_panda && (tesla_hw3 || tesla_hw2)) {
|
||||
return BUILD_SAFETY_CFG(tesla_legacy_pt_rx_checks, TESLA_LEGACY_PT_MSGS);
|
||||
}
|
||||
if (tesla_hw3) {
|
||||
chassis_bus = 1U;
|
||||
return BUILD_SAFETY_CFG(tesla_legacy_hw3_rx_checks, TESLA_TX_LEGACY_MSGS);
|
||||
}
|
||||
if (tesla_hw1) {
|
||||
di_torque1_msg = 0x108U;
|
||||
return BUILD_SAFETY_CFG(tesla_legacy_hw1_rx_checks, TESLA_TX_LEGACY_HW1_MSGS);
|
||||
}
|
||||
return BUILD_SAFETY_CFG(tesla_legacy_hw2_rx_checks, TESLA_TX_LEGACY_MSGS);
|
||||
}
|
||||
|
||||
const safety_hooks tesla_legacy_hooks = {
|
||||
.init = tesla_legacy_init,
|
||||
.rx = tesla_legacy_rx_hook,
|
||||
.tx = tesla_legacy_tx_hook,
|
||||
.fwd = tesla_legacy_fwd_hook,
|
||||
};
|
||||
@@ -235,9 +235,9 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
|
||||
|
||||
// the EPS faults when the steering angle rate is above a certain threshold for too long. to prevent this,
|
||||
// we allow setting STEER_REQUEST bit to 0 while maintaining the requested torque value for a single frame
|
||||
.min_valid_request_frames = 18,
|
||||
.min_valid_request_frames = 17,
|
||||
.max_invalid_request_frames = 1,
|
||||
.min_valid_request_rt_interval = 171000, // 171ms; a ~10% buffer on cutting every 19 frames
|
||||
.min_valid_request_rt_interval = 162000, // 162ms; a ~10% buffer on cutting every 18 frames
|
||||
.has_steer_req_tolerance = true,
|
||||
};
|
||||
|
||||
@@ -404,7 +404,12 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
|
||||
tx = false;
|
||||
}
|
||||
|
||||
if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
|
||||
// Camry Auto Hold replaces the camera AEB message only while stopped.
|
||||
if ((msg->addr == 0x344U) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0)) {
|
||||
if (vehicle_moving || gas_pressed || !acc_main_on) {
|
||||
tx = false;
|
||||
}
|
||||
} else if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
@@ -571,11 +576,21 @@ static safety_config toyota_init(uint16_t param) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool toyota_fwd_hook(int bus_num, int addr) {
|
||||
bool block_msg = false;
|
||||
if (bus_num == 2) {
|
||||
block_msg = (addr == 0x344) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0) &&
|
||||
!vehicle_moving && !gas_pressed && acc_main_on;
|
||||
}
|
||||
return block_msg;
|
||||
}
|
||||
|
||||
const safety_hooks toyota_hooks = {
|
||||
.init = toyota_init,
|
||||
.rx = toyota_rx_hook,
|
||||
.rx_all = toyota_rx_all_hook,
|
||||
.tx = toyota_tx_hook,
|
||||
.fwd = toyota_fwd_hook,
|
||||
.get_checksum = toyota_get_checksum,
|
||||
.compute_checksum = toyota_compute_checksum,
|
||||
.get_quality_flag_valid = toyota_get_quality_flag_valid,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "opendbc/safety/modes/toyota.h"
|
||||
#include "opendbc/safety/modes/tesla.h"
|
||||
#include "opendbc/safety/modes/tesla_preap.h"
|
||||
#include "opendbc/safety/modes/tesla_legacy.h"
|
||||
#include "opendbc/safety/modes/gm.h"
|
||||
#include "opendbc/safety/modes/ford.h"
|
||||
#include "opendbc/safety/modes/hyundai.h"
|
||||
@@ -502,7 +503,8 @@ int set_safety_hooks(uint16_t mode, uint16_t param) {
|
||||
int hook_config_count = sizeof(safety_hook_registry) / sizeof(safety_hook_config);
|
||||
for (int i = 0; i < hook_config_count; i++) {
|
||||
if (safety_hook_registry[i].id == mode) {
|
||||
current_hooks = safety_hook_registry[i].hooks;
|
||||
current_hooks = ((mode == SAFETY_TESLA) && GET_FLAG(param, TESLA_LEGACY_FLAG_HW1)) ?
|
||||
&tesla_legacy_hooks : safety_hook_registry[i].hooks;
|
||||
current_safety_mode = mode;
|
||||
current_safety_param = param;
|
||||
set_status = 0; // set
|
||||
|
||||
@@ -20,6 +20,7 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
|
||||
init_segment(safety, msgs, safety_mode, param)
|
||||
|
||||
rx_tot, rx_invalid, tx_tot, tx_blocked, tx_controls, tx_controls_blocked = 0, 0, 0, 0, 0, 0
|
||||
tx_lateral, tx_lateral_blocked = 0, 0
|
||||
safety_tick_rx_invalid = False
|
||||
blocked_addrs = Counter()
|
||||
invalid_addrs = set()
|
||||
@@ -38,14 +39,20 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
|
||||
if msg.which() == 'sendcan':
|
||||
for canmsg in msg.sendcan:
|
||||
_msg = package_can_msg(canmsg)
|
||||
# TX hooks can revoke permission on a violation. Count the permission
|
||||
# before checking the message, including lateral-only AOL operation.
|
||||
controls_allowed = safety.get_controls_allowed()
|
||||
lateral_allowed = controls_allowed or safety.get_aol_allowed()
|
||||
sent = safety.safety_tx_hook(_msg)
|
||||
if not sent:
|
||||
tx_blocked += 1
|
||||
tx_controls_blocked += safety.get_controls_allowed()
|
||||
tx_controls_blocked += controls_allowed
|
||||
tx_lateral_blocked += lateral_allowed
|
||||
blocked_addrs[canmsg.address] += 1
|
||||
|
||||
carlog.debug("blocked bus %d msg %d at %f" % (canmsg.src, canmsg.address, (msg.logMonoTime - start_t) / 1e9))
|
||||
tx_controls += safety.get_controls_allowed()
|
||||
tx_controls += controls_allowed
|
||||
tx_lateral += lateral_allowed
|
||||
tx_tot += 1
|
||||
elif msg.which() == 'can':
|
||||
# ignore msgs we sent
|
||||
@@ -68,9 +75,11 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
|
||||
print("total msgs with controls allowed:", tx_controls)
|
||||
print("blocked msgs:", tx_blocked)
|
||||
print("blocked with controls allowed:", tx_controls_blocked)
|
||||
print("total msgs with lateral allowed:", tx_lateral)
|
||||
print("blocked with lateral allowed:", tx_lateral_blocked)
|
||||
print("blocked addrs:", blocked_addrs)
|
||||
|
||||
return tx_controls_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
|
||||
return tx_lateral_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def replay_module(monkeypatch):
|
||||
# Load the sibling source explicitly, without native safety libraries or a
|
||||
# host-runtime snapshot. These tests isolate replay accounting, not CAN rules.
|
||||
monkeypatch.setitem(sys.modules, "opendbc.car.carlog", SimpleNamespace(carlog=Mock()))
|
||||
monkeypatch.setitem(sys.modules, "opendbc.safety.tests.libsafety", SimpleNamespace(libsafety_py=SimpleNamespace()))
|
||||
monkeypatch.setitem(sys.modules, "opendbc.safety.tests.safety_replay.helpers",
|
||||
SimpleNamespace(package_can_msg=lambda msg: msg, init_segment=Mock()))
|
||||
spec = importlib.util.spec_from_file_location("replay_drive_accounting", Path(__file__).with_name("replay_drive.py"))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
module.tqdm = lambda msgs: msgs
|
||||
return module
|
||||
|
||||
|
||||
@pytest.mark.parametrize("controls,aol,accepted,post_controls,post_aol", [
|
||||
(False, True, False, False, True), # AOL-only denial must fail replay.
|
||||
(False, True, False, False, False), # A TX hook can revoke AOL permission.
|
||||
(True, False, False, False, False), # A TX hook can revoke controls permission.
|
||||
(False, False, False, False, False), # Expected inactive blocks remain allowed.
|
||||
(False, False, False, True, True), # Post-hook permission must not misclassify a block.
|
||||
(False, True, True, False, True),
|
||||
(True, False, True, True, False),
|
||||
(True, True, True, True, True), # Count overlapping permissions only once.
|
||||
])
|
||||
def test_tx_authorization_accounted_before_hook(replay_module, capsys, controls, aol, accepted, post_controls, post_aol):
|
||||
state = SimpleNamespace(controls=controls, aol=aol)
|
||||
|
||||
def tx_hook(msg):
|
||||
state.controls = post_controls
|
||||
state.aol = post_aol
|
||||
return accepted
|
||||
|
||||
safety = Mock()
|
||||
safety.set_safety_hooks.return_value = 0
|
||||
safety.get_controls_allowed.side_effect = lambda: state.controls
|
||||
safety.get_aol_allowed.side_effect = lambda: state.aol
|
||||
safety.safety_tx_hook.side_effect = tx_hook
|
||||
replay_module.libsafety_py.libsafety = safety
|
||||
packet = SimpleNamespace(address=0x488, src=0, dat=b"\x00" * 4)
|
||||
msg = SimpleNamespace(logMonoTime=0, sendcan=[packet], which=lambda: "sendcan")
|
||||
|
||||
result = replay_module.replay_drive([msg], 10, 0, 0)
|
||||
|
||||
lateral_allowed = controls or aol
|
||||
assert result == (accepted or not lateral_allowed)
|
||||
safety.safety_tx_hook.assert_called_once_with(packet)
|
||||
output = capsys.readouterr().out
|
||||
assert "total openpilot msgs: 1\n" in output
|
||||
assert f"total msgs with controls allowed: {int(controls)}\n" in output
|
||||
assert f"blocked msgs: {int(not accepted)}\n" in output
|
||||
assert f"blocked with controls allowed: {int(controls and not accepted)}\n" in output
|
||||
assert f"total msgs with lateral allowed: {int(lateral_allowed)}\n" in output
|
||||
assert f"blocked with lateral allowed: {int(lateral_allowed and not accepted)}\n" in output
|
||||
@@ -467,6 +467,53 @@ class TestFordCANFDStockSafety(TestFordSafetyBase):
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
|
||||
self.safety.init_tests()
|
||||
|
||||
|
||||
class TestFordMachEExtendedCurvatureSafety(TestFordCANFDStockSafety):
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("ford_lincoln_base_pt")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.ford,
|
||||
FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_CURVATURE)
|
||||
self.safety.init_tests()
|
||||
|
||||
def test_mach_e_extended_curvature_error(self):
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_curvature_measurement(0.0, 12.0)
|
||||
self.assertTrue(self._tx(self._extended_lka_msg()))
|
||||
|
||||
for curvature, allowed in ((0.0058, True), (0.0062, False), (-0.0058, True), (-0.0062, False)):
|
||||
self._set_prev_desired_angle(curvature)
|
||||
self.assertEqual(allowed, self._tx(self._lat_ctl_msg(True, 0.0, 0.0, curvature, 0.0)))
|
||||
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.02, 0.005, 0.0)))
|
||||
|
||||
def test_mach_e_bounded_path_angle_assist(self):
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_curvature_measurement(0.02, 7.5)
|
||||
self._set_prev_desired_angle(0.02)
|
||||
self.assertTrue(self._tx(self._extended_lka_msg()))
|
||||
for path_angle in (0.055, 0.11, 0.15):
|
||||
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, path_angle, 0.02, 0.0)))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.161, 0.02, 0.0)))
|
||||
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, 0.02, 0.0)))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, -0.055, 0.02, 0.0)))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.018, 0.0)))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.12, 0.02, 0.0)))
|
||||
self._reset_curvature_measurement(0.02, 9.0)
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.02, 0.0)))
|
||||
|
||||
def test_other_canfd_fords_keep_original_error(self):
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_curvature_measurement(0.0, 12.0)
|
||||
self.assertTrue(self._tx(self._extended_lka_msg()))
|
||||
self._set_prev_desired_angle(0.0058)
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, 0.0058, 0.0)))
|
||||
self._reset_curvature_measurement(0.02, 7.5)
|
||||
self._set_prev_desired_angle(0.02)
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.02, 0.0)))
|
||||
|
||||
class TestFordStockSafety(TestFordSafetyBase):
|
||||
STEER_MESSAGE = MSG_LateralMotionControl
|
||||
STOCK_LONGITUDINAL = True
|
||||
|
||||
@@ -434,9 +434,11 @@ def test_hyundai_lkas12_tx_requires_stock_camera_message():
|
||||
|
||||
lkas12 = libsafety_py.make_CANPacket(0x53E, 0, bytes(6))
|
||||
assert not safety.safety_tx_hook(lkas12)
|
||||
assert safety.safety_fwd_hook(2, 0x53E) == 0
|
||||
|
||||
safety.safety_rx_hook(libsafety_py.make_CANPacket(0x53E, 2, bytes(6)))
|
||||
assert safety.safety_tx_hook(lkas12)
|
||||
assert safety.safety_fwd_hook(2, 0x53E) == -1
|
||||
|
||||
|
||||
class TestHyundaiLongitudinalSafety(HyundaiLongitudinalBase, TestHyundaiSafety):
|
||||
@@ -633,6 +635,23 @@ class TestHyundaiLongitudinalAolLkasOnEngageSafety(HyundaiAolLkasOnEngageBase, T
|
||||
HyundaiSafetyFlags.LONG | HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE)
|
||||
self.safety.init_tests()
|
||||
|
||||
def test_main_off_after_brake_keeps_lateral_permission(self):
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
|
||||
|
||||
self._rx(self._button_msg(Buttons.NONE, main_button=1))
|
||||
self._rx(self._button_msg(Buttons.NONE, main_button=0))
|
||||
self._rx(self._button_msg(Buttons.SET))
|
||||
self._rx(self._button_msg(Buttons.NONE))
|
||||
self._rx(self._user_brake_msg(True))
|
||||
self._rx(self._button_msg(Buttons.NONE, main_button=1))
|
||||
self._rx(self._button_msg(Buttons.NONE, main_button=0))
|
||||
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertFalse(self.safety.get_acc_main_on())
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
self._set_prev_torque(0)
|
||||
self.assertTrue(self._tx(self._torque_cmd_msg(self.MAX_RATE_UP)))
|
||||
|
||||
|
||||
class TestHyundaiLongitudinalAolMainLkasOnEngageSafety(TestHyundaiLongitudinalSafety):
|
||||
def setUp(self):
|
||||
|
||||
@@ -959,5 +959,85 @@ class TestHyundaiCanfdLKASteeringAolLkasOnEngageEV(HyundaiAolLkasOnEngageStockBa
|
||||
self.safety.init_tests()
|
||||
|
||||
|
||||
class TestSportageNoStockLka(unittest.TestCase):
|
||||
TX_MSGS = None # Supplemental transition tests, not a separate safety mode.
|
||||
PARAM = (HyundaiSafetyFlags.CANFD_LKA_STEERING | HyundaiSafetyFlags.CANFD_LKA_STEERING_ALT |
|
||||
HyundaiSafetyFlags.CANFD_ANGLE_STEERING | HyundaiSafetyFlags.HYBRID_GAS)
|
||||
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.packer = CANPackerSafety("hyundai_canfd_generated")
|
||||
self._init(True)
|
||||
|
||||
def _init(self, suppress):
|
||||
param = self.PARAM | (HyundaiStarPilotSafetyFlags.CANFD_NO_STOCK_LKA if suppress else 0)
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, param)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
|
||||
|
||||
def _speed(self, speed):
|
||||
for _ in range(common.MAX_SAMPLE_VALS):
|
||||
self.safety.safety_rx_hook(self.packer.make_can_msg_safety(
|
||||
"WHEEL_SPEEDS", 1, {f"WHL_Spd{pos}Val": speed for pos in ("FL", "FR", "RL", "RR")}))
|
||||
|
||||
def _toggle(self):
|
||||
for pressed in (1, 0):
|
||||
self.safety.safety_rx_hook(self.packer.make_can_msg_safety("CRUISE_BUTTONS", 1, {"LDA_BTN": pressed}))
|
||||
|
||||
def _steer(self, active, gain=None):
|
||||
return self.packer.make_can_msg_safety("LKAS_ALT", 0, {
|
||||
"LKAS_ANGLE_ACTIVE": 2 if active else 1,
|
||||
"ADAS_StrAnglReqVal": 0,
|
||||
"ADAS_ACIAnglTqRedcGainVal": (0.4 if active else 0.0) if gain is None else gain,
|
||||
"Damping_Gain": 100,
|
||||
})
|
||||
|
||||
def test_stock_scc_buttons_require_engagement(self):
|
||||
resume = self.packer.make_can_msg_safety("CRUISE_BUTTONS", 1, {"CRUISE_BUTTONS": Buttons.RESUME})
|
||||
set_button = self.packer.make_can_msg_safety("CRUISE_BUTTONS", 1, {"CRUISE_BUTTONS": Buttons.SET})
|
||||
self.assertFalse(self.safety.safety_tx_hook(resume))
|
||||
self.assertFalse(self.safety.safety_tx_hook(set_button))
|
||||
|
||||
self.safety.safety_rx_hook(set_button)
|
||||
self.safety.safety_rx_hook(self.packer.make_can_msg_safety("SCC_CONTROL", 1, {"ACCMode": 1}))
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(resume))
|
||||
self.assertTrue(self.safety.safety_tx_hook(set_button))
|
||||
|
||||
self.safety.safety_rx_hook(self.packer.make_can_msg_safety("SCC_CONTROL", 1, {"ACCMode": 0}))
|
||||
self.assertFalse(self.safety.safety_tx_hook(resume))
|
||||
self.assertFalse(self.safety.safety_tx_hook(set_button))
|
||||
|
||||
def test_aol_toggle_keeps_stock_blocked_and_inactive_status_allowed(self):
|
||||
self._speed(30)
|
||||
for expected_aol in (False, True, False, True, False):
|
||||
if expected_aol != self.safety.get_aol_allowed():
|
||||
self._toggle()
|
||||
self.assertEqual(expected_aol, self.safety.get_aol_allowed())
|
||||
for addr in (0x110, 0x362):
|
||||
self.assertEqual(-1, self.safety.safety_fwd_hook(2, addr))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self._steer(False)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x362, 32)))
|
||||
self.assertEqual(expected_aol, self.safety.safety_tx_hook(self._steer(True)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self._steer(False, gain=0.4)))
|
||||
|
||||
def test_standstill_does_not_allow_active_steering(self):
|
||||
self._speed(0)
|
||||
self._toggle()
|
||||
self.assertTrue(self.safety.get_aol_allowed())
|
||||
self.assertFalse(self.safety.safety_tx_hook(self._steer(True)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self._steer(False)))
|
||||
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x110))
|
||||
|
||||
def test_unflagged_handoff_and_reinitialization_unchanged(self):
|
||||
self._init(False)
|
||||
self._speed(30)
|
||||
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x110))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self._steer(False)))
|
||||
self._toggle()
|
||||
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x110))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self._steer(True)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import create_gas_interceptor_command
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.test_hyundai import checksum
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param", [0x9405, 0x9C05, 0x9401, 0x1005, 0])
|
||||
def test_ray_pedal_tx_isolation_and_limits(param):
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, param)
|
||||
safety.init_tests()
|
||||
safety.set_controls_allowed(True)
|
||||
packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
|
||||
def tx(gas):
|
||||
addr, dat, bus = create_gas_interceptor_command(packer, gas, 3)
|
||||
return safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
has_ray_signature = param in (0x9405, 0x9C05)
|
||||
assert tx(0) is has_ray_signature
|
||||
assert tx(0.55) is has_ray_signature
|
||||
assert not tx(0.56)
|
||||
assert not tx(0.70)
|
||||
assert not tx(1.0)
|
||||
|
||||
if has_ray_signature:
|
||||
safety.set_controls_allowed(False)
|
||||
assert tx(0)
|
||||
assert not tx(0.1)
|
||||
safety.set_controls_allowed(True)
|
||||
safety.set_gas_pressed_prev(True)
|
||||
assert not tx(0.1)
|
||||
safety.set_gas_pressed_prev(False)
|
||||
addr, dat, bus = create_gas_interceptor_command(packer, 0.1, 3)
|
||||
bad_crc = bytearray(dat)
|
||||
bad_crc[-1] ^= 1
|
||||
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(bad_crc)))
|
||||
|
||||
|
||||
def test_ray_pedal_rx_crc_is_checked_only_for_ray_signature():
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
|
||||
safety.init_tests()
|
||||
dat = bytes.fromhex("01f403d55de8")
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x201, 0, dat))
|
||||
bad_crc = bytearray(dat)
|
||||
bad_crc[-1] ^= 1
|
||||
assert not safety.safety_rx_hook(libsafety_py.make_CANPacket(0x201, 0, bytes(bad_crc)))
|
||||
|
||||
|
||||
def test_ray_native_commanded_gas_does_not_cancel_driver_override_safety():
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
|
||||
safety.init_tests()
|
||||
safety.set_controls_allowed(True)
|
||||
packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
|
||||
physical_rest = bytes.fromhex("010801f30cef")
|
||||
native_gas = bytes.fromhex("004e008000ae0700")
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x201, 0, physical_rest))
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x371, 0, native_gas))
|
||||
assert not safety.get_gas_pressed_prev()
|
||||
addr, dat, bus = create_gas_interceptor_command(packer, 0.1, 3)
|
||||
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
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,
|
||||
})
|
||||
press_addr, press_dat, press_bus = physical_press
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(press_addr, press_bus, press_dat))
|
||||
assert safety.get_gas_pressed_prev()
|
||||
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
|
||||
def test_non_ray_hyundai_ev_keeps_native_driver_gas_detection():
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x1001)
|
||||
safety.init_tests()
|
||||
native_gas = bytes.fromhex("004e008000ae0700")
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x371, 0, native_gas))
|
||||
assert safety.get_gas_pressed_prev()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("controls_allowed", [False, True])
|
||||
def test_ray_native_cruise_cancel_allowed_during_pedal_override(controls_allowed):
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
|
||||
safety.init_tests()
|
||||
safety.set_controls_allowed(controls_allowed)
|
||||
safety.set_gas_pressed_prev(True)
|
||||
packer = CANPacker("hyundai_can_refresh_generated")
|
||||
addr, dat, bus = packer.make_can_msg("CLU11", 0, {"CF_Clu_CruiseSwState": 4})
|
||||
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
pedal_packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
addr, dat, bus = create_gas_interceptor_command(pedal_packer, 0.1, 3)
|
||||
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
|
||||
def test_ray_standstill_launch_obeys_hardware_brake_override():
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
|
||||
safety.init_tests()
|
||||
packer = CANPacker("hyundai_can_refresh_generated")
|
||||
pedal_packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
|
||||
def rx(name, values):
|
||||
addr, dat, bus = checksum(packer.make_can_msg(name, 0, values))
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
def tx(gas):
|
||||
addr, dat, bus = create_gas_interceptor_command(pedal_packer, gas, 0)
|
||||
return safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
rx("WHL_SPD11", {"WHL_SPD_FL": 0, "WHL_SPD_RR": 0})
|
||||
assert not safety.get_vehicle_moving()
|
||||
rx("TCS13", {"DriverOverride": 2})
|
||||
safety.set_controls_allowed(True)
|
||||
assert safety.get_brake_pressed_prev()
|
||||
assert tx(0)
|
||||
assert not tx(0.012)
|
||||
rx("TCS13", {"DriverOverride": 0})
|
||||
assert not safety.get_brake_pressed_prev()
|
||||
assert tx(0.012)
|
||||
rx("TCS13", {"DriverOverride": 2})
|
||||
assert not tx(0.012)
|
||||
assert tx(0)
|
||||
@@ -0,0 +1,107 @@
|
||||
import pytest
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.subaru.avh import AVH_REQUEST, AVH_STATUS, INPUTS, avh_request, checksum
|
||||
from opendbc.car.subaru.tests.test_avh import sample
|
||||
from opendbc.car.subaru.values import SubaruSafetyFlags
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
FLAGS = int(SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.FIXED_ANGLE_LIMITS |
|
||||
SubaruSafetyFlags.STOP_START_BUTTON | SubaruSafetyFlags.AVH_STARTUP)
|
||||
|
||||
|
||||
def packet(address, data, bus=1):
|
||||
return libsafety_py.make_CANPacket(address, bus, data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def safety():
|
||||
s = libsafety_py.libsafety
|
||||
s.set_timer(0)
|
||||
assert s.set_safety_hooks(CarParams.SafetyModel.subaru, FLAGS) == 0
|
||||
s.set_controls_allowed(False)
|
||||
for tick in range(101):
|
||||
s.set_timer(tick * 100_000)
|
||||
for address in INPUTS:
|
||||
if address != AVH_REQUEST or tick % 10 == 0:
|
||||
assert s.safety_rx_hook(packet(address, sample(address, tick // 10 if address == AVH_REQUEST else tick)))
|
||||
return s
|
||||
|
||||
|
||||
def request(step=1):
|
||||
return avh_request(sample(AVH_REQUEST, 10), step)[1]
|
||||
|
||||
|
||||
def test_pair_and_third_frame_blocked(safety):
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_050_000)
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
safety.set_timer(10_100_000)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('byte', range(8))
|
||||
def test_payload_mutation_blocked(safety, byte):
|
||||
data = bytearray(request())
|
||||
data[byte] ^= 4
|
||||
if byte:
|
||||
data[0] = checksum(AVH_REQUEST, data)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, data))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bus', [0, 2])
|
||||
def test_wrong_bus_blocked(safety, bus):
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(), bus))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('delay', [44_999, 80_001])
|
||||
def test_followup_timing(safety, delay):
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_000_000 + delay)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('address,offset,value', [(AVH_REQUEST, 2, 1), (AVH_STATUS, 5, 32),
|
||||
(0x40, 4, 1), (0x48, 3, 3), (0x13A, 2, 1)])
|
||||
def test_abort_on_manual_ack_or_movement(safety, address, offset, value):
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_050_000)
|
||||
data = bytearray(sample(address, 11 if address == AVH_REQUEST else 101))
|
||||
data[offset] = value
|
||||
data[0] = checksum(address, data)
|
||||
assert safety.safety_rx_hook(packet(address, data))
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
|
||||
|
||||
def test_stale_template_and_status_tx_blocked(safety):
|
||||
assert not safety.safety_tx_hook(packet(AVH_STATUS, sample(AVH_STATUS, 1)))
|
||||
safety.set_timer(10_030_001)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('flags', [FLAGS & ~1024, FLAGS | 32, FLAGS | 2, FLAGS & ~16, FLAGS | 512])
|
||||
def test_permission_gates(safety, flags):
|
||||
assert safety.set_safety_hooks(CarParams.SafetyModel.subaru, flags) == 0
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reason', ['new_template', 'corrupt', 'engaged', 'expired', 'duplicate'])
|
||||
def test_extra_failure_gates(safety, reason):
|
||||
if reason == 'engaged':
|
||||
safety.set_controls_allowed(True)
|
||||
elif reason == 'expired':
|
||||
safety.set_timer(30_000_001)
|
||||
elif reason == 'duplicate':
|
||||
safety.set_timer(10_040_000)
|
||||
assert safety.safety_rx_hook(packet(AVH_REQUEST, sample(AVH_REQUEST, 10)))
|
||||
elif reason == 'corrupt':
|
||||
data = bytearray(sample(AVH_REQUEST, 11))
|
||||
data[0] ^= 1
|
||||
safety.safety_rx_hook(packet(AVH_REQUEST, data))
|
||||
else:
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_050_000)
|
||||
assert safety.safety_rx_hook(packet(AVH_REQUEST, sample(AVH_REQUEST, 11)))
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
return
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user