Keep radar failures from disabling independent lateral steering

This commit is contained in:
firestar5683
2026-10-31 17:25:44 -05:00
parent ddd67e58bc
commit 23123aefb4
4 changed files with 192 additions and 12 deletions
+16 -10
View File
@@ -57,7 +57,7 @@ from openpilot.starpilot.conditional_mode.projection import paired_clocks_ns
from openpilot.starpilot.conditional_mode.runtime_settings import (ConditionalSettingsOwner, experimental_requested, read_mode_sources)
from openpilot.starpilot.conditional_mode.status import settings_fingerprint
from openpilot.starpilot.feature_runtime import enabled as feature_enabled
from openpilot.starpilot.nostalgia import aol_no_entry, paddle_cancel, physical_cancel, saved_enabled as nostalgia_saved_enabled
from openpilot.starpilot.nostalgia import LONGITUDINAL_ONLY_EVENTS, aol_no_entry, paddle_cancel, physical_cancel, saved_enabled as nostalgia_saved_enabled
from openpilot.starpilot.lateral.low_speed_advisory import LowSpeedAdvisory, advisory_eligible
from openpilot.starpilot.lateral.lane_change_status_wire import alert_wording, decode as decode_lane_status, fresh_for_model
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert
@@ -265,7 +265,7 @@ class SelfdriveD:
if not_running != self.not_running_prev:
cloudlog.event("process_not_running", not_running=not_running, error=True)
self.not_running_prev = not_running
self.events.add(EventName.processNotRunning)
self.events.add(EventName.radarFault if set(not_running) == {"radard"} else EventName.processNotRunning)
return True
return False
@@ -506,7 +506,7 @@ class SelfdriveD:
self.events.add(EventName.selfdrivedLagging)
if self.CP.openpilotLongitudinalControl:
if self.sm['radarState'].radarErrors.canError:
self.events.add(EventName.canError)
self.events.add(EventName.radarFault)
elif self.sm['radarState'].radarErrors.radarUnavailableTemporary:
self.events.add(EventName.radarTempUnavailable)
elif any(self.sm['radarState'].radarErrors.to_dict().values()):
@@ -621,8 +621,11 @@ class SelfdriveD:
no_system_errors = (not has_disable_events) or (hardware_events_added == 0 and len(self.events) == num_can_events)
warmup_sec = 5.
big_model_settling = self.big_model_loading or time.monotonic() < self.big_model_ready_t + warmup_sec
if not self.sm.all_checks() and no_system_errors and not big_model_settling: # the load holds modelV2 and friends back on purpose
if not self.sm.all_alive():
if (not self.sm.all_checks() and not big_model_settling and
(no_system_errors or any(event in LONGITUDINAL_ONLY_EVENTS for event in self.events.names))): # the load holds modelV2 and friends back on purpose
if self.sm.all_checks([service for service in self.sm.services if service != 'radarState']):
self.events.add(EventName.radarFault)
elif not self.sm.all_alive():
self.events.add(EventName.commIssue)
elif not self.sm.all_freq_ok():
self.events.add(EventName.commIssueAvgFreq)
@@ -1707,6 +1710,7 @@ class SelfdriveD:
lateral_events = [event for event in lateral_events if event not in (EventName.wrongGear, EventName.reverseGear)]
return decide_axes(
standard_lateral=self.active, standard_longitudinal=bool(self.enabled and self.CP.openpilotLongitudinalControl and
not any(event in LONGITUDINAL_ONLY_EVENTS for event in self.events.names) and
not any(sample.seatbeltUnlatched or sample.doorOpen for sample, _ in getattr(self, 'car_state_samples', ()))),
intent=intent, native=native, car_state=CS, initialized=self.initialized,
model_ready=bool(self.sm.all_checks(['modelV2', 'extrinsicsCalibration']) and
@@ -1989,7 +1993,7 @@ class SelfdriveD:
expired_healthy_source=getattr(self, 'aol_expired_healthy_source', False),
previously_active=previous.lateral_active, hard_fault=bool(
self.aol_selected_pair_future or self.aol_authority_lost or lost_active_aol or lost_permission or self.events.contains(ET.IMMEDIATE_DISABLE) or
self.events.contains(ET.SOFT_DISABLE)),
any(event.softDisable and event.name not in LONGITUDINAL_ONLY_EVENTS for event in self.events.to_msg())),
model_ready=bool(self.sm.all_checks(['modelV2', 'extrinsicsCalibration']) and
self.sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.calibrated),
user_disable=self.events.contains(ET.USER_DISABLE))
@@ -2057,7 +2061,7 @@ class SelfdriveD:
from openpilot.starpilot.aol.seatbelt import seatbelt_lateral_soft_event_allowed
reverse_pause = known_reverse_pause(CS, getattr(self, 'car_state_samples', ()))
aol_hard_soft_fault = any(event.softDisable and not (
event.name == EventName.doorOpen or
event.name == EventName.doorOpen or event.name in LONGITUDINAL_ONLY_EVENTS or
event.name == EventName.wrongGear and reverse_pause or
event.name == EventName.steerTempUnavailable and CS.steerFaultTemporary and not CS.steerFaultPermanent or
seatbelt_lateral_soft_event_allowed(self.CP, CS, intent, native, event,
@@ -2075,7 +2079,8 @@ class SelfdriveD:
self.events.add(EventName.controlsMismatch)
lost_active_aol |= pause
was_enabled = self.enabled
if self.aol_replay and source_current and CS.doorOpen and was_enabled:
if self.aol_replay and source_current and was_enabled and (
CS.doorOpen or any(event in LONGITUDINAL_ONLY_EVENTS for event in self.events.names)):
# Ordinary cruise is withdrawn; its legacy permission is no longer expected.
self.aol_availability_long_inhibit = True
if not self.CP.passive and self.initialized:
@@ -2146,9 +2151,10 @@ class SelfdriveD:
previous.lateral_active and previous.native_acknowledged and not self.aol_axis_decision.lateral_active and
self.aol_fault_reason in ('critical', 'availabilityPause') and not self.aol_authority_lost and
intent is not None and intent.allowedLatch and
any(event.softDisable and event.name != EventName.doorOpen for event in self.events.to_msg()) and
any(event.softDisable and event.name not in (EventName.doorOpen, *LONGITUDINAL_ONLY_EVENTS)
for event in self.events.to_msg()) and
not self.events.contains(ET.IMMEDIATE_DISABLE)):
# Door-open does not inhibit independent steering; ordinary LONG keeps its alert.
# Lateral-only operation keeps ordinary longitudinal faults visible.
self.aol_soft_takeover_alert = True
if (self.aol_replay and intent is not None and intent.lateralArmed and
CS.steerFaultTemporary and not CS.steerFaultPermanent and not CS.steeringPressed):
@@ -0,0 +1,170 @@
"""Production shared event/axis/Controls callers with controlled native peers."""
from contextlib import ExitStack
import os
from unittest import mock
import pytest
from openpilot.cereal import messaging
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.selfdrived.events import EventName
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
from openpilot.starpilot.aol.tests.test_vehicle_policy_cache import profile
FAMILIES = ('sdgm', 'ascm', 'flashed', 'bolt2017', 'bolt2018', 'bolt2022', 'boltacc',
'honda', 'hyundai', 'toyota', 'ford', 'mazda', 'tesla')
def params(family):
from opendbc.car.gm.values import CAR
if family == 'sdgm':
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
return sdgm_params(alpha=True, sascm=True, radar=True)
if family == 'ascm':
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
return ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True, radar=True)
if family == 'flashed':
from opendbc.car.gm.tests.test_volt_flashed_obd import params as flashed
return flashed()
if family.startswith('bolt'):
from opendbc.car.gm.tests.test_bolt_pedal import params as bolt
candidate = {'bolt2017': CAR.CHEVROLET_BOLT_CC_2017, 'bolt2018': CAR.CHEVROLET_BOLT_CC_2018_2021,
'bolt2022': CAR.CHEVROLET_BOLT_CC_2022_2023, 'boltacc': CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL}[family]
return bolt(candidate, pedal=True, alpha_long=True, camera=True)
return profile(family)
def callers(cleanup, family, full_op=False):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
cleanup.callback(case.doCleanups)
cleanup.enter_context(OpenpilotPrefix())
cleanup.enter_context(mock.patch.dict(os.environ, {'SIMULATION': '0', 'AOL_REPLAY_RUNTIME': '1'}))
return case._authority_callers(full_op, cp=params(family), genuine_events=True)
def radar_fault(sd, error, *, other=None):
original = sd.update_events
def update(cs):
radar = messaging.new_message('radarState', valid=True)
setattr(radar.radarState.radarErrors, error, True)
sd.sm.data['radarState'] = radar.radarState.as_reader()
original(cs)
# Stock-long ports do not consume radarState, but share lateral event policy.
if not sd.CP.openpilotLongitudinalControl:
sd.events.add(EventName.radarTempUnavailable if error == 'radarUnavailableTemporary' else EventName.radarFault)
if other is not None:
sd.events.add(other)
return mock.patch.object(sd, 'update_events', side_effect=update)
@pytest.mark.parametrize('family', FAMILIES)
@pytest.mark.parametrize('error', ('radarFault', 'radarUnavailableTemporary', 'canError'))
def test_radar_only_fault_keeps_lateral_for_every_port(family, error):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, family)
for tick in (0, 1):
axis, _, command = step(tick, native_request_long=False, panda_controls_allowed=False)
assert axis.lateralActive and command.latActive
expected = EventName.radarTempUnavailable if error == 'radarUnavailableTemporary' else EventName.radarFault
with radar_fault(sd, error):
for tick in range(2, 22):
axis, host, command = step(tick, native_request_long=False, panda_controls_allowed=False)
assert expected in sd.events.names
assert axis.lateralActive and command.latActive
assert not host.enabled and not command.longActive and not axis.longitudinalActive
assert not sd.aol_authority_lost and str(axis.faultReason) == 'none'
assert EventName.controlsMismatch not in sd.events.names
@pytest.mark.parametrize('family', ('sdgm', 'ascm', 'bolt2018', 'boltacc', 'honda'))
def test_radar_withdraws_long_immediately_without_lateral_loss_or_auto_resume(family):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, family, full_op=True)
assert sd.CP.openpilotLongitudinalControl
cs = fixture.car_state()
cs.cruiseState.enabled = True
for tick in (0, 1):
axis, host, command = step(tick, observed_state=cs)
assert host.enabled and command.longActive and command.latActive
with radar_fault(sd, 'canError'):
for tick in range(2, 322):
axis, host, command = step(tick, observed_state=cs, native_request_long=False,
long_allowed=False, panda_controls_allowed=False)
assert EventName.radarFault in sd.events.names and EventName.canError not in sd.events.names
assert not host.enabled and not command.longActive and not axis.longitudinalActive
assert axis.lateralActive and command.latActive
assert EventName.controlsMismatch not in sd.events.names
for tick in range(322, 330):
axis, host, command = step(tick, native_request_long=False, long_allowed=False, panda_controls_allowed=False)
assert command.latActive and not command.longActive and not host.enabled
@pytest.mark.parametrize('fault', ('canError', 'canBusMissing', 'lowMemory', 'steerUnavailable', 'modeldLagging'))
def test_radar_never_hides_other_faults(fault):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, 'sdgm')
step(0, native_request_long=False)
with radar_fault(sd, 'canError', other=getattr(EventName, fault)):
axis, _, command = step(1, native_request_long=False)
assert not command.latActive and not axis.lateralActive
assert EventName.radarFault in sd.events.names and getattr(EventName, fault) in sd.events.names
@pytest.mark.parametrize('negative', ({'allowed': False}, {'native_valid': False}, {'panda_fault': True}, {'calibrated': False}))
def test_radar_cannot_bypass_native_or_model_health(negative):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, 'sdgm')
step(0, native_request_long=False)
with radar_fault(sd, 'canError'):
for tick in range(1, 31):
axis, _, command = step(tick, native_request_long=False, **negative)
assert not axis.lateralActive and not command.latActive
@pytest.mark.parametrize('service', ('radarState', 'modelV2'))
@pytest.mark.parametrize('health', ('valid', 'alive', 'freq_ok'))
def test_only_radar_service_failure_is_longitudinal(service, health):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, 'sdgm')
step(0, native_request_long=False)
original = sd.update_events
def update(cs):
getattr(sd.sm, health)[service] = False
original(cs)
with mock.patch.object(sd, 'update_events', side_effect=update):
axis, _, command = step(1, native_request_long=False)
if service == 'radarState':
assert EventName.radarFault in sd.events.names
assert axis.lateralActive and command.latActive
else:
assert not axis.lateralActive and not command.latActive
@pytest.mark.parametrize('names,expected', ((['radard'], EventName.radarFault),
(['radard', 'modeld'], EventName.processNotRunning)))
def test_dead_radard_does_not_hide_other_processes(names, expected):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, 'sdgm')
step(0, native_request_long=False)
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.driving_process_failures', return_value=names):
axis, _, command = step(1, native_request_long=False)
assert expected in sd.events.names
assert bool(axis.lateralActive and command.latActive) == (names == ['radard'])
@pytest.mark.parametrize('family', ('sdgm', 'honda'))
def test_radar_fault_present_at_start_does_not_require_prior_engagement(family):
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, family)
with radar_fault(sd, 'radarFault'):
for tick in range(20):
axis, host, command = step(tick, native_request_long=False, panda_controls_allowed=False)
assert axis.lateralActive and command.latActive and not host.enabled and not command.longActive
def test_radar_exception_does_not_arm_unrequested_aol():
with ExitStack() as cleanup:
sd, _, step = callers(cleanup, 'sdgm', full_op=True)
step(0, latched=False)
with radar_fault(sd, 'radarFault'):
axis, host, command = step(1, latched=False, native_request_long=False, panda_controls_allowed=False)
assert not axis.lateralActive and not command.latActive and not host.enabled and not command.longActive
+5 -1
View File
@@ -7,6 +7,9 @@ from openpilot.starpilot.longitudinal.ioniq6_start import eligible as ioniq6_lon
from openpilot.starpilot.saved_source import read_saved
LONGITUDINAL_ONLY_EVENTS = (EventName.radarFault, EventName.radarTempUnavailable)
ButtonType = car.CarState.ButtonEvent.Type
@@ -40,7 +43,8 @@ def physical_cancel(cs, cp=None) -> bool:
def aol_no_entry(event_names, cs, *, paddle_only_cancel: bool, cruise_main_required: bool = True,
allow_below_engage_speed: bool = False, allow_lateral_cancel: bool = False) -> bool:
"""Apply vehicle-owned lateral exceptions without changing ordinary cruise events."""
return any(event not in (EventName.pedalPressed, EventName.seatbeltNotLatched, EventName.resumeBlocked, EventName.doorOpen) and
return any(event not in (EventName.pedalPressed, EventName.seatbeltNotLatched, EventName.resumeBlocked, EventName.doorOpen,
*LONGITUDINAL_ONLY_EVENTS) and
ET.NO_ENTRY in EVENTS.get(event, {}) and
(event != EventName.belowEngageSpeed or not allow_below_engage_speed) and
(event != EventName.wrongCarMode or cruise_main_required) and
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "59cca55bbc8f9f8d42bee954161022f1014f9087f480d4b0b77ff6b27116542a", "schema_version": 1}
{"protected_inventory_sha256": "fbedd0567ba8be7ec8af9fb677658ab0c903b46a4a4975b74ebcefd02289d021", "schema_version": 1}