Fix Tesla cooperative steering saturation warnings

This commit is contained in:
root
2026-09-12 19:15:55 +01:00
parent 185c501809
commit 9136f1cb62
12 changed files with 1109 additions and 7 deletions
+11
View File
@@ -375,6 +375,17 @@ struct CarControl {
torqueOutputCan @8: Float32; # value sent over can to the car
speed @6: Float32; # m/s
lateralControlMode @9: LateralControlMode;
steeringLimitInfo @10 :SteeringLimitInfo;
struct SteeringLimitInfo {
valid @0 :Bool;
modelLimitErrorDeg @1 :Float32;
resumeLimitErrorDeg @2 :Float32;
cooperativeLimitErrorDeg @3 :Float32;
cooperativeOffsetDeg @4 :Float32;
monoTime @5 :UInt64;
combinedLimitErrorDeg @6 :Float32;
}
enum LongControlState @0xe40f3a917d908282{
off @0;
@@ -24,6 +24,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
@@ -39,8 +40,28 @@ class CarController(CarControllerBase):
from opendbc.car.tesla.interface import CarInterface
self.VM = VehicleModel(CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP))
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 _write_steering_limit_info(self, actuators):
info = actuators.steeringLimitInfo
info.valid = self.steering_limit_info_valid
info.modelLimitErrorDeg = self.model_limit_error_deg
info.resumeLimitErrorDeg = self.resume_limit_error_deg
info.cooperativeLimitErrorDeg = self.cooperative_limit_error_deg
info.cooperativeOffsetDeg = self.cooperative_offset_deg
info.monoTime = self.steering_limit_mono_time
info.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 +69,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,6 +82,27 @@ 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,
)
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()
can_sends.append(self.tesla_can.create_steering_control(self.apply_angle_command_last, lat_active))
if self.frame % 10 == 0:
@@ -79,6 +125,7 @@ class CarController(CarControllerBase):
# TODO: HUD control
new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_command_last
self._write_steering_limit_info(new_actuators)
self.frame += 1
return new_actuators, can_sends
@@ -121,6 +168,7 @@ class CarController(CarControllerBase):
new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_last
self._write_steering_limit_info(new_actuators)
self.frame += 1
return new_actuators, can_sends
@@ -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
File diff suppressed because one or more lines are too long
@@ -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,221 @@
import json
import math
from pathlib import Path
from types import SimpleNamespace
import pytest
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, TeslaSafetyFlags
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 legacy_actuator_dict(actuators):
values = actuators.to_dict()
values.pop("steeringLimitInfo", None)
return values
def test_steering_limit_info_defaults_to_invalid():
actuators = car.CarControl.Actuators.new_message()
assert not actuators.steeringLimitInfo.valid
def test_steering_limit_info_round_trips_through_car_output():
output = car.CarOutput.new_message()
info = output.actuatorsOutput.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
with car.CarOutput.from_bytes(output.to_bytes()) as restored:
restored_info = restored.actuatorsOutput.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_publishes_r_n_a_t_f_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 = actuators.steeringLimitInfo
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 = actuators.steeringLimitInfo
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 = actuators.steeringLimitInfo
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 = first.steeringLimitInfo.to_dict()
second, _ = run_frame(controller, -40.0, torque=-1.5, now_nanos=1_010_000_000)
assert second.steeringLimitInfo.to_dict() == first_info
assert second.steeringLimitInfo.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 active.steeringLimitInfo.valid
inactive, _ = run_frame(controller, 8.0, torque=0.9, lat_active=False, now_nanos=1_010_000_000)
assert not inactive.steeringLimitInfo.valid
assert inactive.steeringLimitInfo.monoTime == 0
resumed, _ = run_frame(controller, 8.0, torque=0.9, now_nanos=1_020_000_000)
assert resumed.steeringLimitInfo.valid
assert resumed.steeringLimitInfo.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)
assert not actuators.steeringLimitInfo.valid
assert actuators.steeringLimitInfo.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"]
+14 -3
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import math
from numbers import Number
import os
import time
from cereal import car, custom, log
import cereal.messaging as messaging
@@ -26,7 +28,8 @@ from openpilot.selfdrive.controls.lib.drive_helpers import (
from openpilot.selfdrive.controls.lib.lane_centering import LaneCenteringController
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
from openpilot.selfdrive.controls.lib.steering_saturation import is_angle_steering_limited
from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature
from openpilot.selfdrive.controls.lib.latcontrol_torque import (
BOLT_2018_2021_STEER_RATIO_TEST_SCALE,
@@ -48,6 +51,7 @@ LaneChangeDirection = log.LaneChangeDirection
LateralControlMode = car.CarControl.Actuators.LateralControlMode
ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys())
REPLAY = "REPLAY" in os.environ
# After a smoothed lane change ends, ramp the curvature limits back to stock over this
# time so the final recenter correction is shaped instead of stepping through unclamped.
@@ -873,8 +877,15 @@ class Controls:
if self.sm['selfdriveState'].active:
CO = self.sm['carOutput']
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.steer_limited_by_safety = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \
STEER_ANGLE_SATURATION_THRESHOLD
output_healthy = (
self.sm.valid['carOutput'] and
self.sm.alive['carOutput'] and
self.sm.freq_ok['carOutput']
)
now_nanos = self.sm.logMonoTime['selfdriveState'] if REPLAY else time.monotonic_ns()
self.steer_limited_by_safety = is_angle_steering_limited(
self.CP, CC.actuators.steeringAngleDeg, CO, output_healthy, now_nanos,
)
else:
self.steer_limited_by_safety = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2
+1 -3
View File
@@ -3,9 +3,7 @@ import math
from cereal import log
from opendbc.car.subaru.values import CAR as SUBARU_CAR
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
# TODO This is speed dependent
STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
from openpilot.selfdrive.controls.lib.steering_saturation import STEER_ANGLE_SATURATION_THRESHOLD
_ASCENT_ANGLE_TRACKING_GAIN = 0.25
_ASCENT_ANGLE_TRACKING_MAX_CORRECTION = 8.0
@@ -0,0 +1,49 @@
import math
from opendbc.car.tesla.values import CAR, TeslaSafetyFlags
# TODO This is speed dependent
STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
_MAX_STEERING_LIMIT_INFO_AGE_NANOS = 100_000_000
def _legacy_angle_steering_limited(requested_angle: float, car_output) -> bool:
return abs(requested_angle - car_output.actuatorsOutput.steeringAngleDeg) > \
STEER_ANGLE_SATURATION_THRESHOLD
def is_angle_steering_limited(CP, requested_angle: float, car_output, output_healthy: bool, now_nanos: int) -> bool:
legacy_limited = _legacy_angle_steering_limited(requested_angle, car_output)
try:
cooperative_enabled = any(
safety_config.safetyParam & TeslaSafetyFlags.COOP_STEERING.value
for safety_config in CP.safetyConfigs
)
if CP.carFingerprint != CAR.TESLA_MODEL_3 or not cooperative_enabled or not output_healthy:
return legacy_limited
info = car_output.actuatorsOutput.steeringLimitInfo
errors = (
info.modelLimitErrorDeg,
info.resumeLimitErrorDeg,
info.cooperativeLimitErrorDeg,
info.combinedLimitErrorDeg,
)
numeric_values = errors + (info.cooperativeOffsetDeg,)
mono_time = info.monoTime
if not info.valid or mono_time <= 0:
return legacy_limited
age_nanos = now_nanos - mono_time
if age_nanos < 0 or age_nanos > _MAX_STEERING_LIMIT_INFO_AGE_NANOS:
return legacy_limited
if not all(math.isfinite(value) for value in numeric_values) or any(error < 0 for error in errors):
return legacy_limited
return max(errors) > STEER_ANGLE_SATURATION_THRESHOLD
except (AttributeError, TypeError, ValueError, OverflowError):
return legacy_limited
@@ -0,0 +1,161 @@
import math
import pytest
from cereal import car
from opendbc.car.tesla.interface import CarInterface
from opendbc.car.tesla.values import CAR, TeslaSafetyFlags
from openpilot.selfdrive.controls.lib.steering_saturation import is_angle_steering_limited
REQUESTED_ANGLE = -14.5
OUTPUT_ANGLE = -9.0
SAMPLE_TIME_NANOS = 1_000_000_000
FRESH_TIME_NANOS = SAMPLE_TIME_NANOS + 20_000_000
MAX_DIAGNOSTIC_AGE_NANOS = 100_000_000
NEXT_FLOAT32_AFTER_2_5 = 2.500000238418579
ERROR_FIELDS = (
"modelLimitErrorDeg",
"resumeLimitErrorDeg",
"cooperativeLimitErrorDeg",
"combinedLimitErrorDeg",
)
NUMERIC_FIELDS = ERROR_FIELDS + ("cooperativeOffsetDeg",)
def make_case(candidate=CAR.TESLA_MODEL_3, cooperative_enabled=True):
cp = CarInterface.get_non_essential_params(candidate)
for safety_config in cp.safetyConfigs:
if cooperative_enabled:
safety_config.safetyParam |= TeslaSafetyFlags.COOP_STEERING.value
else:
safety_config.safetyParam &= ~TeslaSafetyFlags.COOP_STEERING.value
output = car.CarOutput.new_message()
output.actuatorsOutput.steeringAngleDeg = OUTPUT_ANGLE
info = output.actuatorsOutput.steeringLimitInfo
info.valid = True
info.monoTime = SAMPLE_TIME_NANOS
info.cooperativeOffsetDeg = 5.5
return cp, output
def detect(cp, output, requested_angle=REQUESTED_ANGLE, output_healthy=True, now_nanos=FRESH_TIME_NANOS):
return is_angle_steering_limited(cp.as_reader(), requested_angle, output.as_reader(), output_healthy, now_nanos)
def test_light_offset_does_not_count_as_limiting():
cp, output = make_case()
assert not detect(cp, output)
@pytest.mark.parametrize("field", ERROR_FIELDS)
def test_each_genuine_limit_is_visible_during_cooperation(field):
cp, output = make_case()
setattr(output.actuatorsOutput.steeringLimitInfo, field, 3.0)
assert detect(cp, output)
def test_combined_small_limits_remain_visible():
cp, output = make_case()
info = output.actuatorsOutput.steeringLimitInfo
info.modelLimitErrorDeg = 1.5
info.cooperativeLimitErrorDeg = 1.5
info.combinedLimitErrorDeg = 3.0
assert detect(cp, output)
@pytest.mark.parametrize(("error", "expected"), (
(2.5, False),
(NEXT_FLOAT32_AFTER_2_5, True),
))
def test_diagnostic_threshold_is_strictly_greater(error, expected):
cp, output = make_case()
output.actuatorsOutput.steeringLimitInfo.modelLimitErrorDeg = error
assert detect(cp, output) is expected
@pytest.mark.parametrize("age_nanos", (0, MAX_DIAGNOSTIC_AGE_NANOS))
def test_diagnostic_age_bounds_are_inclusive(age_nanos):
cp, output = make_case()
assert not detect(cp, output, now_nanos=SAMPLE_TIME_NANOS + age_nanos)
def test_signed_cooperative_offset_is_valid_data():
cp, output = make_case()
output.actuatorsOutput.steeringLimitInfo.cooperativeOffsetDeg = -5.5
assert not detect(cp, output)
@pytest.mark.parametrize("scenario", (
"default_message",
"invalid_flag",
"zero_timestamp",
"future_timestamp",
"stale_timestamp",
"unhealthy_output",
"unsupported_model",
"cooperative_mode_disabled",
))
def test_unusable_diagnostics_keep_legacy_warning(scenario):
cp, output = make_case()
output_healthy = True
now_nanos = FRESH_TIME_NANOS
if scenario == "default_message":
output = car.CarOutput.new_message()
output.actuatorsOutput.steeringAngleDeg = OUTPUT_ANGLE
elif scenario == "invalid_flag":
output.actuatorsOutput.steeringLimitInfo.valid = False
elif scenario == "zero_timestamp":
output.actuatorsOutput.steeringLimitInfo.monoTime = 0
elif scenario == "future_timestamp":
output.actuatorsOutput.steeringLimitInfo.monoTime = now_nanos + 1
elif scenario == "stale_timestamp":
output.actuatorsOutput.steeringLimitInfo.monoTime = now_nanos - MAX_DIAGNOSTIC_AGE_NANOS - 1
elif scenario == "unhealthy_output":
output_healthy = False
elif scenario == "unsupported_model":
cp, output = make_case(CAR.TESLA_MODEL_Y)
elif scenario == "cooperative_mode_disabled":
cp, output = make_case(cooperative_enabled=False)
assert detect(cp, output, output_healthy=output_healthy, now_nanos=now_nanos)
@pytest.mark.parametrize(("requested_angle", "expected"), (
(OUTPUT_ANGLE - 2.5, False),
(OUTPUT_ANGLE - NEXT_FLOAT32_AFTER_2_5, True),
))
def test_fallback_preserves_legacy_strict_threshold(requested_angle, expected):
cp, output = make_case()
output.actuatorsOutput.steeringLimitInfo.valid = False
assert detect(cp, output, requested_angle=requested_angle) is expected
@pytest.mark.parametrize("field", NUMERIC_FIELDS)
@pytest.mark.parametrize(("bad_value", "requested_angle", "legacy_result"), (
(math.nan, REQUESTED_ANGLE, True),
(math.inf, OUTPUT_ANGLE, False),
(-math.inf, OUTPUT_ANGLE, False),
))
def test_nonfinite_diagnostic_values_use_legacy_fallback(field, bad_value, requested_angle, legacy_result):
cp, output = make_case()
setattr(output.actuatorsOutput.steeringLimitInfo, field, bad_value)
assert detect(cp, output, requested_angle=requested_angle) is legacy_result
@pytest.mark.parametrize("field", ERROR_FIELDS)
def test_negative_error_values_use_legacy_fallback(field):
cp, output = make_case()
setattr(output.actuatorsOutput.steeringLimitInfo, field, -0.1)
assert detect(cp, output)
@@ -0,0 +1,153 @@
from types import SimpleNamespace
import pytest
from cereal import car, custom, log
from opendbc.car.tesla.interface import CarInterface
from opendbc.car.tesla.values import CAR, TeslaSafetyFlags
from openpilot.selfdrive.controls import controlsd
from openpilot.selfdrive.controls.controlsd import Controls
REQUESTED_ANGLE = -14.5
OUTPUT_ANGLE = -9.0
SAMPLE_TIME_NANOS = 1_000_000_000
FRESH_TIME_NANOS = SAMPLE_TIME_NANOS + 20_000_000
STALE_TIME_NANOS = SAMPLE_TIME_NANOS + 100_000_001
HOST_TIME_NANOS = 9_000_000_000
CAR_STATE_TIME_NANOS = 7_000_000_000
class CapturePubMaster:
def __init__(self):
self.sent = {}
def send(self, service, message):
self.sent[service] = message
class PublishSubMaster:
def __init__(self, car_output, selfdrive_time_nanos, output_healthy=True):
car_state = car.CarState.new_message()
car_state.canValid = True
long_plan = log.LongitudinalPlan.new_message()
long_plan.speeds = []
selfdrive_state = log.SelfdriveState.new_message()
selfdrive_state.active = True
self.messages = {
"carState": car_state.as_reader(),
"longitudinalPlan": long_plan.as_reader(),
"starpilotCarState": custom.StarPilotCarState.new_message().as_reader(),
"selfdriveState": selfdrive_state.as_reader(),
"carOutput": car_output.as_reader(),
"driverAssistance": log.DriverAssistance.new_message().as_reader(),
"driverMonitoringState": log.DriverMonitoringState.new_message().as_reader(),
}
self.valid = {"carOutput": output_healthy, "driverAssistance": False}
self.alive = {"carOutput": output_healthy}
self.freq_ok = {"carOutput": output_healthy}
self.logMonoTime = {
"selfdriveState": selfdrive_time_nanos,
"carState": CAR_STATE_TIME_NANOS,
"longitudinalPlan": 0,
"modelV2": 0,
}
def __getitem__(self, service):
return self.messages[service]
def make_car_params():
cp = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_3)
cp.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.COOP_STEERING.value
return cp.as_reader()
def make_car_output(real_limit_error=0.0):
output = car.CarOutput.new_message()
output.actuatorsOutput.steeringAngleDeg = OUTPUT_ANGLE
info = output.actuatorsOutput.steeringLimitInfo
info.valid = True
info.monoTime = SAMPLE_TIME_NANOS
info.cooperativeOffsetDeg = 5.5
info.modelLimitErrorDeg = real_limit_error
info.combinedLimitErrorDeg = real_limit_error
return output
def make_controls(car_output, selfdrive_time_nanos, output_healthy=True):
controls = Controls.__new__(Controls)
controls.CP = make_car_params()
controls.sm = PublishSubMaster(car_output, selfdrive_time_nanos, output_healthy)
controls.pm = CapturePubMaster()
controls.curvature = 0.0
controls.calibrated_pose = None
controls.desired_curvature = 0.0
controls.LoC = SimpleNamespace(
long_control_state=car.CarControl.Actuators.LongControlState.off,
pid=SimpleNamespace(p=0.0, i=0.0, f=0.0),
)
controls.LaC = SimpleNamespace()
controls.starpilot_toggles = SimpleNamespace()
controls.steer_limited_by_safety = False
return controls
def run_publish(monkeypatch, replay, selfdrive_time_nanos, host_time_nanos=HOST_TIME_NANOS,
output_healthy=True, real_limit_error=0.0):
monkeypatch.setattr(controlsd, "REPLAY", replay, raising=False)
monkeypatch.setattr(controlsd.time, "monotonic_ns", lambda: host_time_nanos)
controls = make_controls(make_car_output(real_limit_error), selfdrive_time_nanos, output_healthy)
cc = car.CarControl.new_message()
cc.enabled = True
cc.latActive = True
cc.actuators.steeringAngleDeg = REQUESTED_ANGLE
lac_log = log.ControlsState.LateralAngleState.new_message()
controls.publish(cc, lac_log)
return controls
def test_replay_uses_current_poll_timestamp_for_fresh_diagnostics(monkeypatch):
controls = run_publish(monkeypatch, True, FRESH_TIME_NANOS)
assert not controls.steer_limited_by_safety
def test_replay_stale_diagnostics_still_use_legacy_fallback(monkeypatch):
controls = run_publish(monkeypatch, True, STALE_TIME_NANOS)
assert controls.steer_limited_by_safety
@pytest.mark.parametrize(("selfdrive_time_nanos", "expected_limited"), (
(SAMPLE_TIME_NANOS + 100_000_000, False),
(SAMPLE_TIME_NANOS + 100_000_001, True),
(SAMPLE_TIME_NANOS - 1, True),
(0, True),
))
def test_replay_publish_preserves_diagnostic_age_boundaries(monkeypatch, selfdrive_time_nanos, expected_limited):
controls = run_publish(monkeypatch, True, selfdrive_time_nanos)
assert controls.steer_limited_by_safety is expected_limited
def test_live_uses_monotonic_clock_instead_of_message_clock(monkeypatch):
controls = run_publish(monkeypatch, False, FRESH_TIME_NANOS, host_time_nanos=STALE_TIME_NANOS)
assert controls.steer_limited_by_safety
def test_unhealthy_car_output_still_uses_legacy_fallback(monkeypatch):
controls = run_publish(monkeypatch, True, FRESH_TIME_NANOS, output_healthy=False)
assert controls.steer_limited_by_safety
def test_replay_genuine_limiter_error_remains_visible(monkeypatch):
controls = run_publish(monkeypatch, True, FRESH_TIME_NANOS, real_limit_error=3.0)
assert controls.steer_limited_by_safety
@@ -0,0 +1,331 @@
from types import SimpleNamespace
import cereal.messaging as messaging
import pytest
from cereal import car, custom, log
from opendbc.car import DT_CTRL, 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
from opendbc.car.vehicle_model import VehicleModel
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
from openpilot.selfdrive.controls.lib.steering_saturation import is_angle_steering_limited
from openpilot.selfdrive.selfdrived.events import ET
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
REPRO_REQUESTED_ANGLE = -14.505379676818848
REPRO_MEASURED_ANGLE = -10.0
REPRO_SPEED = 12.509657859802246
REPRO_TORQUE = 0.8999999761581421
REPRO_DESIRED_CURVATURE = 0.006728461943566799
REPRO_ACTUAL_CURVATURE = 0.004746654070913792
START_NANOS = 1_000_000_000
def make_params():
toggles = SimpleNamespace(tesla_cooperative_steering=True, trailer_load_kg=0.0)
return CarInterface.get_params(CAR.TESLA_MODEL_3, gen_empty_fingerprint(), [], False, False, False, toggles)
def make_controller_state(torque, speed, measured_angle, steering_disengage=False):
return SimpleNamespace(
out=SimpleNamespace(
steeringTorque=torque,
steeringAngleDeg=measured_angle,
steeringDisengage=steering_disengage,
vEgo=speed,
vEgoRaw=speed,
gasPressed=False,
),
hands_on_level=0,
das_control={"DAS_controlCounter": 0},
)
def make_car_control(requested_angle, lat_active=True):
control = car.CarControl.new_message()
control.enabled = lat_active
control.latActive = lat_active
control.actuators.steeringAngleDeg = requested_angle
return control
def run_controller_frame(controller, requested_angle, torque, speed, measured_angle, frame,
lat_active=True, steering_disengage=False):
now_nanos = START_NANOS + frame * 10_000_000
actuators, _ = controller.update(
make_car_control(requested_angle, lat_active).as_reader(),
make_controller_state(torque, speed, measured_angle, steering_disengage),
now_nanos,
SimpleNamespace(),
)
event = messaging.new_message("carOutput", valid=True)
event.carOutput.actuatorsOutput = actuators
restored = messaging.log_from_bytes(event.to_bytes())
return restored.carOutput, now_nanos
def make_lateral_car_state(speed, measured_angle):
state = car.CarState.new_message()
state.canValid = True
state.vEgo = speed
state.vEgoRaw = speed
state.steeringAngleDeg = measured_angle
state.gearShifter = car.CarState.GearShifter.drive
state.cruiseState.available = True
state.cruiseState.enabled = True
return state
def advance_angle_counter(controller, CP, state, limited, desired_curvature):
live_params = log.LiveParametersData.new_message()
live_params.steerRatio = CP.steerRatio
live_params.stiffnessFactor = 1.0
live_params.roll = 0.0
live_params.angleOffsetDeg = 0.0
_, _, angle_log = controller.update(
True, state.as_reader(), VehicleModel(CP), live_params.as_reader(), limited,
desired_curvature, False, 0.2, None, None, SimpleNamespace(),
)
return angle_log
def controls_state_with_angle_log(angle_log, actual_curvature):
controls_state = log.ControlsState.new_message()
controls_state.curvature = actual_curvature
controls_state.lateralControlState.angleState = angle_log
return controls_state
def configure_selfdrived(CP):
fpcp = custom.StarPilotCarParams.new_message()
Params().put("StarPilotCarParams", fpcp.to_bytes())
selfdrived = SelfdriveD(CP.as_reader())
selfdrived.initialized = True
selfdrived.enabled = True
selfdrived.active = True
selfdrived.startup_event = None
selfdrived.last_steering_pressed_frame = 0
selfdrived.sm.frame = 300
for service in selfdrived.sm.data:
selfdrived.sm.valid[service] = True
selfdrived.sm.alive[service] = True
selfdrived.sm.freq_ok[service] = True
selfdrived.sm.seen[service] = True
selfdrived.sm.updated[service] = False
selfdrived.sm.recv_frame[service] = selfdrived.sm.frame
device_state = log.DeviceState.new_message()
device_state.started = True
device_state.freeSpacePercent = 100.0
device_state.memoryUsagePercent = 0
selfdrived.sm.data["deviceState"] = device_state.as_reader()
live_calibration = log.LiveCalibrationData.new_message()
live_calibration.calStatus = log.LiveCalibrationData.Status.calibrated
selfdrived.sm.data["liveCalibration"] = live_calibration.as_reader()
live_parameters = log.LiveParametersData.new_message()
live_parameters.valid = True
selfdrived.sm.data["liveParameters"] = live_parameters.as_reader()
panda_states = messaging.new_message("pandaStates", 0)
selfdrived.sm.data["pandaStates"] = panda_states.pandaStates
return selfdrived
def run_selfdrived_warning_path(selfdrived, state, angle_log, actual_curvature, desired_curvature,
requested_angle=REPRO_REQUESTED_ANGLE):
controls_state = controls_state_with_angle_log(angle_log, actual_curvature)
model = log.ModelDataV2.new_message()
model.action.desiredCurvature = desired_curvature
selfdrived.sm.data["controlsState"] = controls_state.as_reader()
selfdrived.sm.data["modelV2"] = model.as_reader()
selfdrived.sm.data["carControl"] = make_car_control(requested_angle).as_reader()
selfdrived.CS_prev = state.as_reader()
selfdrived.update_events(state.as_reader())
alerts = selfdrived.events.create_alerts([ET.WARNING])
return selfdrived.events.names, [alert.alert_type for alert in alerts]
def test_recorded_light_torque_reproduction_no_longer_reaches_warning():
CP = make_params()
tesla_controller = CarController(DBC[CAR.TESLA_MODEL_3], CP)
lateral_state = make_lateral_car_state(REPRO_SPEED, REPRO_MEASURED_ANGLE)
baseline_counter = LatControlAngle(CP.as_reader(), None, DT_CTRL)
corrected_counter = LatControlAngle(CP.as_reader(), None, DT_CTRL)
baseline_log = corrected_log = None
output = None
now_nanos = 0
for frame in range(260):
output, now_nanos = run_controller_frame(
tesla_controller, REPRO_REQUESTED_ANGLE, REPRO_TORQUE, REPRO_SPEED, REPRO_MEASURED_ANGLE, frame,
)
if frame >= 210:
legacy_limited = abs(REPRO_REQUESTED_ANGLE - output.actuatorsOutput.steeringAngleDeg) > 2.5
corrected_limited = is_angle_steering_limited(
CP.as_reader(), REPRO_REQUESTED_ANGLE, output, True, now_nanos,
)
baseline_log = advance_angle_counter(
baseline_counter, CP, lateral_state, legacy_limited, REPRO_DESIRED_CURVATURE,
)
corrected_log = advance_angle_counter(
corrected_counter, CP, lateral_state, corrected_limited, REPRO_DESIRED_CURVATURE,
)
info = output.actuatorsOutput.steeringLimitInfo
errors = (info.modelLimitErrorDeg, info.resumeLimitErrorDeg,
info.cooperativeLimitErrorDeg, info.combinedLimitErrorDeg)
assert info.valid
assert info.cooperativeOffsetDeg > 2.5
assert max(errors) <= 2.5
assert baseline_log.saturated
assert not corrected_log.saturated
selfdrived = configure_selfdrived(CP)
baseline_events, baseline_alert_types = run_selfdrived_warning_path(
selfdrived, lateral_state, baseline_log, REPRO_ACTUAL_CURVATURE, REPRO_DESIRED_CURVATURE,
)
assert log.OnroadEvent.EventName.steerSaturated in baseline_events
assert "steerSaturated/warning" in baseline_alert_types
selfdrived.sm.frame += 1
events, alert_types = run_selfdrived_warning_path(
selfdrived, lateral_state, corrected_log, REPRO_ACTUAL_CURVATURE, REPRO_DESIRED_CURVATURE,
)
assert log.OnroadEvent.EventName.steerSaturated not in events
assert "steerSaturated/warning" not in alert_types
def test_persistent_real_model_limiting_with_light_torque_still_warns():
CP = make_params()
tesla_controller = CarController(DBC[CAR.TESLA_MODEL_3], CP)
speed = 22.0
requested_angle = 40.0
lateral_state = make_lateral_car_state(speed, 0.0)
angle_counter = LatControlAngle(CP.as_reader(), None, DT_CTRL)
angle_log = None
output = None
for frame in range(60):
output, now_nanos = run_controller_frame(
tesla_controller, requested_angle, REPRO_TORQUE, speed, 0.0, frame,
)
limited = is_angle_steering_limited(CP.as_reader(), requested_angle, output, True, now_nanos)
angle_log = advance_angle_counter(angle_counter, CP, lateral_state, limited, 0.01)
info = output.actuatorsOutput.steeringLimitInfo
assert info.valid
assert info.modelLimitErrorDeg > 2.5
assert info.combinedLimitErrorDeg > 2.5
assert angle_log.saturated
selfdrived = configure_selfdrived(CP)
events, alert_types = run_selfdrived_warning_path(
selfdrived, lateral_state, angle_log, 0.003, 0.01, requested_angle,
)
assert log.OnroadEvent.EventName.steerSaturated in events
assert "steerSaturated/warning" in alert_types
def test_default_old_output_uses_legacy_warning_path():
CP = make_params()
output_event = messaging.new_message("carOutput", valid=True)
output_event.carOutput.actuatorsOutput.steeringAngleDeg = REPRO_MEASURED_ANGLE
output = messaging.log_from_bytes(output_event.to_bytes()).carOutput
lateral_state = make_lateral_car_state(REPRO_SPEED, REPRO_MEASURED_ANGLE)
angle_counter = LatControlAngle(CP.as_reader(), None, DT_CTRL)
for frame in range(50):
limited = is_angle_steering_limited(
CP.as_reader(), REPRO_REQUESTED_ANGLE, output, True, START_NANOS + frame * 10_000_000,
)
angle_log = advance_angle_counter(
angle_counter, CP, lateral_state, limited, REPRO_DESIRED_CURVATURE,
)
assert not output.actuatorsOutput.steeringLimitInfo.valid
assert angle_log.saturated
selfdrived = configure_selfdrived(CP)
events, _ = run_selfdrived_warning_path(
selfdrived, lateral_state, angle_log, REPRO_ACTUAL_CURVATURE, REPRO_DESIRED_CURVATURE,
)
assert log.OnroadEvent.EventName.steerSaturated in events
def test_stale_real_offset_sample_uses_legacy_warning_path():
CP = make_params()
tesla_controller = CarController(DBC[CAR.TESLA_MODEL_3], CP)
lateral_state = make_lateral_car_state(REPRO_SPEED, REPRO_MEASURED_ANGLE)
angle_counter = LatControlAngle(CP.as_reader(), None, DT_CTRL)
for frame in range(260):
output, now_nanos = run_controller_frame(
tesla_controller, REPRO_REQUESTED_ANGLE, REPRO_TORQUE, REPRO_SPEED, REPRO_MEASURED_ANGLE, frame,
)
assert not is_angle_steering_limited(CP.as_reader(), REPRO_REQUESTED_ANGLE, output, True, now_nanos)
stale_now_nanos = output.actuatorsOutput.steeringLimitInfo.monoTime + 100_000_001
for _ in range(50):
limited = is_angle_steering_limited(
CP.as_reader(), REPRO_REQUESTED_ANGLE, output, True, stale_now_nanos,
)
angle_log = advance_angle_counter(
angle_counter, CP, lateral_state, limited, REPRO_DESIRED_CURVATURE,
)
assert angle_log.saturated
selfdrived = configure_selfdrived(CP)
events, _ = run_selfdrived_warning_path(
selfdrived, lateral_state, angle_log, REPRO_ACTUAL_CURVATURE, REPRO_DESIRED_CURVATURE,
)
assert log.OnroadEvent.EventName.steerSaturated in events
def test_inactive_interval_clears_diagnostics_before_reengagement():
CP = make_params()
tesla_controller = CarController(DBC[CAR.TESLA_MODEL_3], CP)
active, _ = run_controller_frame(tesla_controller, 0.0, REPRO_TORQUE, REPRO_SPEED, 0.0, 0)
inactive, _ = run_controller_frame(
tesla_controller, 0.0, REPRO_TORQUE, REPRO_SPEED, 0.0, 1, lat_active=False,
)
resumed, resumed_now = run_controller_frame(
tesla_controller, 0.0, REPRO_TORQUE, REPRO_SPEED, 0.0, 2,
)
overridden, _ = run_controller_frame(
tesla_controller, 0.0, REPRO_TORQUE, REPRO_SPEED, 0.0, 3, steering_disengage=True,
)
assert active.actuatorsOutput.steeringLimitInfo.valid
assert not inactive.actuatorsOutput.steeringLimitInfo.valid
assert inactive.actuatorsOutput.steeringLimitInfo.monoTime == 0
assert resumed.actuatorsOutput.steeringLimitInfo.valid
assert resumed.actuatorsOutput.steeringLimitInfo.monoTime == resumed_now
assert not overridden.actuatorsOutput.steeringLimitInfo.valid
assert overridden.actuatorsOutput.steeringLimitInfo.monoTime == 0
@pytest.mark.parametrize(("fault_field", "expected_event"), (
("steerFaultTemporary", log.OnroadEvent.EventName.steerTempUnavailableSilent),
("steerFaultPermanent", log.OnroadEvent.EventName.steerUnavailable),
))
def test_existing_steering_fault_events_remain_present(fault_field, expected_event):
CP = make_params()
state = make_lateral_car_state(REPRO_SPEED, REPRO_MEASURED_ANGLE)
setattr(state, fault_field, True)
angle_counter = LatControlAngle(CP.as_reader(), None, DT_CTRL)
angle_log = advance_angle_counter(
angle_counter, CP, state, False, REPRO_DESIRED_CURVATURE,
)
selfdrived = configure_selfdrived(CP)
events, _ = run_selfdrived_warning_path(
selfdrived, state, angle_log, REPRO_ACTUAL_CURVATURE, REPRO_DESIRED_CURVATURE,
)
assert expected_event in events