Preserve Subaru intent through stop and source handoffs

This commit is contained in:
firestar5683
2026-11-03 18:30:19 -06:00
parent dd4214f8f5
commit 89af8a6569
5 changed files with 70 additions and 32 deletions
@@ -47,12 +47,14 @@ def stock_lateral_role(self, CS, native, intent=None, *, allow_independent_cance
settings = getattr(self, 'aol_settings', None)
previous = getattr(self, 'aol_axis_decision', None)
from opendbc.car.gm.aol import qualified_gm
from opendbc.car.subaru.stock_aol import qualified as subaru_qualified
gas_role = bool((self.CP.brand == 'hyundai' and len(self.CP.safetyConfigs) == 1 and
stock_canfd_qualified(self.CP, marked_only=True) or self.CP.brand == 'gm' and qualified_gm(self.CP) and self.CP.pcmCruise and
not self.CP.openpilotLongitudinalControl) and settings is not None and settings.enabled and
not self.CP.openpilotLongitudinalControl or
self.CP.brand == 'subaru' and subaru_qualified(self.CP, marked_only=True)) and settings is not None and settings.enabled and
CS.gasPressed and not CS.brakePressed and not CS.regenBraking and
previous is not None and (previous.lateral_active or self.CP.brand == 'gm' and
previous is not None and (previous.lateral_active or self.CP.brand in ('gm', 'subaru') and
getattr(self, 'aol_fault_reason', 'none') == 'availabilityPause' and previous.desired_lateral) and
previous.native_acknowledged and not previous.longitudinal_active and
(CS.gearShifter == car.CarState.GearShifter.drive or self.CP.brand == 'gm' and
+9 -5
View File
@@ -123,8 +123,10 @@ def stock_lateral_off(cp, cs, native, *, allow_independent_cancel=False, allow_i
from openpilot.starpilot.car.toyota.aol import qualified as toyota_qualified
from opendbc.car.gm.aol import qualified_gm
from opendbc.car.subaru.stock_aol import qualified as subaru_qualified
gm_stock_gas = bool(cp.brand == 'gm' and qualified_gm(cp) and cp.pcmCruise and not cp.openpilotLongitudinalControl)
stock_gas = bool(cp.brand == 'gm' and qualified_gm(cp) and cp.pcmCruise and not cp.openpilotLongitudinalControl or
cp.brand == 'subaru' and subaru_qualified(cp, marked_only=True))
independent_angle = angle_qualified(cp, marked_only=True)
independent_brake = independent_angle or (allow_independent_brake and (
toyota_qualified(cp, marked_only=True) and not cs.cruiseState.enabled or
@@ -135,7 +137,7 @@ def stock_lateral_off(cp, cs, native, *, allow_independent_cancel=False, allow_i
not cs.accFaulted and not cs.doorOpen and not cs.seatbeltUnlatched and
(not cs.brakePressed or independent_brake and allow_independent_brake) and
not cs.regenBraking and (not cs.gasPressed or independent_angle or
allow_independent_gas and (gm_stock_gas or toyota_qualified(cp, marked_only=True) or
allow_independent_gas and (stock_gas or toyota_qualified(cp, marked_only=True) or
cp.brand == 'hyundai' and stock_canfd_qualified(cp, marked_only=True))) and
(not physical_cancel(cs, cp) or independent_angle and allow_independent_cancel) and
native is not None and native.requestedLateral and native.lateralAllowed and not native.requestedLongitudinal)
@@ -874,14 +876,16 @@ class TransportPauseFeedback:
not cs.regenBraking and self.lateral_only_baseline and
not native.requestedLongitudinal and not native.longitudinalAllowed)
from opendbc.car.gm.aol import qualified_gm
gm_stock_gas = bool(cp.brand == 'gm' and qualified_gm(cp) and cp.pcmCruise and not cp.openpilotLongitudinalControl and
from opendbc.car.subaru.stock_aol import qualified as subaru_qualified
stock_gas = bool((cp.brand == 'gm' and qualified_gm(cp) and cp.pcmCruise and not cp.openpilotLongitudinalControl or
cp.brand == 'subaru' and subaru_qualified(cp, marked_only=True)) and
cs.gasPressed and not cs.brakePressed and not cs.regenBraking and self.lateral_only_baseline and
not native.requestedLongitudinal and not native.longitudinalAllowed)
stock_user = stock_lateral_user_event(cp)
stock_lateral = bool(reason == 'availabilityPause' and axis.sourceCarStateMonoTime == 0 and
not axis.lateralActive and not axis.longitudinalActive and not axis.desiredLongitudinal and
stock_lateral_off(cp, cs, native, allow_independent_brake=toyota_pedal,
allow_independent_gas=gm_stock_gas or toyota_pedal and cs.gasPressed) and previous is not None and
allow_independent_gas=stock_gas or toyota_pedal and cs.gasPressed) and previous is not None and
previous[0] == self.session and previous[7] == self.session and
previous[14] == intent_producer_session and previous[15] == self.availability_epoch and
(previous[11] and not previous[12] and previous[13] and not previous[10] and
@@ -1184,7 +1188,7 @@ class TransportPauseFeedback:
return False
native = _configured_transport_native(sm, cp, self.session, now_ns, allow_restricted_lateral=True)
return bool(native is not None and native.pandaInventory and native.requestedLateral and
not native.lateralAllowed and
(not native.lateralAllowed or subaru_stop and not axis.desiredLateral and not axis.lateralActive) and
(not native.requestedLongitudinal and not combined_stop or
combined_stop and native.requestedLongitudinal and native.longitudinalAllowed) and
native.observedMonoTime >= self.lateral_native_ns and
@@ -4,9 +4,11 @@ from unittest.mock import patch
from openpilot.cereal import messaging
def selfdrive_step(sd, published, native, previous_controls, cp, safety, now, *, calibration):
def selfdrive_step(sd, published, native, previous_controls, cp, safety, now, *, calibration, source_timeout=False):
peers = []
for service in sd.sm.data:
if source_timeout and service in ('carState', 'carOutput', 'aolIntentWire'):
continue
if service in ('alertDebug', 'lateralManeuverPlan', 'laneChangeAssistWire', 'starpilotLongitudinalPlan'):
continue
if service == 'aolIntentWire':
@@ -60,7 +62,7 @@ def selfdrive_step(sd, published, native, previous_controls, cp, safety, now, *,
patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic', return_value=now / 1e9),
patch('openpilot.selfdrive.selfdrived.selfdrived.time.clock_gettime_ns', return_value=now),
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', return_value=None),
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=published['carState']),
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=None if source_timeout else published['carState']),
patch.object(sd.sm, 'update', side_effect=lambda _: sd.sm.update_msgs(now / 1e9, peers)),
patch.object(sd.pm, 'send', side_effect=capture),
):
@@ -24,7 +24,8 @@ from openpilot.starpilot.car.subaru.tests.native_fixture import subaru_native_pa
@pytest.mark.parametrize('candidate', [CAR.SUBARU_ASCENT_2023, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_FORESTER, CAR.SUBARU_OUTBACK])
def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate, subaru_native_pair, tmp_path, aol=True, manual=False, stop_resume=False,
hard_stop_native_first=False, main_only=False, preferences=None, firmware=None,
deadline_gap=False, fixed_angle_handoff=False):
deadline_gap=False, fixed_angle_handoff=False, host_first_stop=False,
source_outage=False):
fw = firmware_records(candidate, FW_VERSIONS, 'subaru') if firmware is None else firmware
rows = []
with actual_factory(candidate, gen_empty_fingerprint(), fw, '0' * 17, manual=manual, aol=aol, preferences=preferences) as (card, params, matches):
@@ -64,6 +65,8 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
ticks = (list(range(125)) + list(range(125, 135)) * 30 + list(range(135, 460))
if hard_stop_native_first else range(460))
gap_tick = 75 if main_only else 210
last_card_published = {}
pending_host_can = []
for elapsed, tick in enumerate(ticks):
now = base + elapsed * 10_000_000 + (22_504_479 if deadline_gap and elapsed >= gap_tick else 0)
cruise = not main_only and (30 <= tick < 110 or tick >= 410)
@@ -71,9 +74,11 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
reverse = 150 <= tick < 185
temporary = 225 <= tick < 260
main = not 310 <= tick < 350 and (tick >= 30 or not main_only)
stopped = stop_resume and (125 <= tick < 135 if hard_stop_native_first else 185 <= tick < 195)
first_resume = 145 <= tick < 150 if hard_stop_native_first else 125 <= tick < 145
stopped = stop_resume and (125 <= tick < 135 if hard_stop_native_first or host_first_stop else 185 <= tick < 195)
first_resume = 145 <= tick < 150 if hard_stop_native_first or host_first_stop else 125 <= tick < 145
source_missing = bool(source_outage and 75 <= tick < 81)
values = {
'Throttle': {'Throttle_Pedal': 100 if source_outage and 60 <= tick < 90 else 0},
'Steering_Torque': {'Steer_Warning': int(temporary),
'Steer_Torque_Sensor': (250 if 70 <= tick < 76 else 143 if 76 <= tick < 90 else 0) if fixed_angle_handoff else 0},
'Steering_2': {'Steering_Angle': (5.65 if tick < 78 else 5.61) if fixed_angle_handoff else 0},
@@ -85,11 +90,13 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
'Transmission': {'Gear': 3 if reverse else 121},
}
incoming = stream.frames(values, tick=tick)
if source_outage:
pending_host_can.append((now, incoming))
safety.set_timer((now // 1000) & 0xFFFFFFFF)
for addr, data, bus in incoming:
safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data))
safety.safety_tick()
hard_stop_receipt = hard_stop_native_first and elapsed == 125
hard_stop_receipt = (hard_stop_native_first or host_first_stop) and elapsed == 125
if hard_stop_receipt:
# Native zero wheels and its USB poll precede the host's stopped CarState.
receipt, hard_order = pandad.step(driven, published, now)
@@ -110,7 +117,7 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
if 'carControl' in latest_controls:
peers.append(latest_controls['carControl'])
peers.extend(driven_new[s] for s in ('onroadEvents', 'selfdriveState', 'aolAxisState') if s in driven_new and s in card.sm.data)
if receipt is not None and (tick % 10 == 1 or hard_stop_receipt):
if receipt is not None and (tick % 10 == 1 or hard_stop_receipt and not host_first_stop or host_first_stop and elapsed == 127):
peers.append(receipt.as_reader())
card.sm.update_msgs((now + 1000) / 1e9, peers)
published = {}
@@ -123,25 +130,30 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
wire = msg if isinstance(msg, bytes) else msg.to_bytes()
_published[service] = messaging.log_from_bytes(wire)
with (
patch.object(card.sm, 'update'),
patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[b'can']),
patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=[(now, incoming)]),
patch('openpilot.selfdrive.car.card.REPLAY', False),
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now + 2000),
patch('openpilot.selfdrive.car.card.time.monotonic', return_value=(now + 2000) / 1e9),
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=now + 2000),
patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=stamped),
patch.object(card.pm, 'send', side_effect=capture),
):
card.step()
if source_missing:
published = {key: value for key, value in last_card_published.items() if key != 'sendcan'}
else:
with (
patch.object(card.sm, 'update'),
patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[b'can']),
patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=list(pending_host_can) if source_outage else [(now, incoming)]),
patch('openpilot.selfdrive.car.card.REPLAY', False),
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now + 2000),
patch('openpilot.selfdrive.car.card.time.monotonic', return_value=(now + 2000) / 1e9),
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=now + 2000),
patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=stamped),
patch.object(card.pm, 'send', side_effect=capture),
):
card.step()
last_card_published = dict(published)
pending_host_can.clear()
if tick % 10 == 0:
published['pandaStates'] = panda.as_reader()
cs = published['carState'].carState
assert cs.brakePressed == brake and cs.gearShifter == (structs.CarState.GearShifter.reverse if reverse else structs.CarState.GearShifter.drive)
if tick >= 10:
assert cs.canValid and safety.safety_config_valid()
if hard_stop_receipt:
if hard_stop_receipt and not host_first_stop:
_assert_hard_stop_fault_guards(card, cs, now + 2000)
# Deliver each genuine10Hz native receipt once; do not refresh receive time/frequency from a cached snapshot.
new_native = receipt if receipt is not None and (tick % 10 == 1 or hard_stop_receipt) else None
@@ -151,7 +163,8 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
with patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=now + 2000):
expired, _ = controls.state_control()
assert not expired.latActive and not expired.longActive
driven_new = selfdrive_step(sd, published, new_native, previous_controls, cp, safety, now, calibration='calibrated')
driven_new = selfdrive_step(sd, published, new_native, previous_controls, cp, safety, now, calibration='calibrated',
source_timeout=source_missing)
if deadline_gap and elapsed == gap_tick:
current = driven_new['aolAxisState'].aolAxisState
assert current.lateralActive and not sd.aol_authority_lost, 'SUBARU_FRESH_SUCCESSOR_LOST_LATERAL'
@@ -202,7 +215,8 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
request = bool(decoder.vl[steering_name]['LKAS_Request'])
output = decoder.vl[steering_name]['LKAS_Output']
decoded_steering.append({'request': request, 'output': output, 'nativeAccepted': accepted})
if not stopped and (65 <= tick < 90 or aol and (first_resume or 205 <= tick < 220 or 280 <= tick < 300)):
if not stopped and (65 <= tick < 90 and not (source_outage and 75 <= tick < 84) or
aol and (first_resume or 205 <= tick < 220 or 280 <= tick < 300)):
if fixed_angle_handoff and 72 <= tick < 78:
assert not request and accepted, (tick, decoded_steering)
else:
@@ -241,11 +255,15 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
}
rows.append(row)
if stopped:
assert not row['lat'] and row['cardLatch'] and row['permission'] == 0 and row['fault'] == 'none', row
assert not row['lat'] and row['cardLatch'] and row['permission'] == 0 and row['fault'] == 'none', ('SUBARU_STOP_LOST_ARMED_INTENT', row)
if source_outage and 78 <= tick < 81:
assert not row['lat'] and not row['long'] and row['fault'] == 'availabilityPause', ('SUBARU_SOURCE_GAP_BECAME_CRITICAL', row)
if source_outage and 84 <= tick < 90:
assert row['lat'] and row['cardLatch'] and row['permission'] == 1 and not row['controls'], row
assert not command.longActive, row
if 10 <= tick < 30:
assert not command.latActive and not bool(intent and intent.allowedLatch) and row['permission'] == 0, row
if 65 <= tick < 90:
if 65 <= tick < 90 and not (source_outage and 75 <= tick < 84):
assert command.latActive and bool(intent and intent.allowedLatch) == aol and row['permission'] == int(aol), row
if not stopped and (first_resume or 205 <= tick < 220 or 280 <= tick < 300):
assert command.latActive == aol and bool(intent and intent.allowedLatch) == aol and row['permission'] == int(aol) and not row['controls'], row
@@ -388,3 +406,15 @@ def test_actual_subaru_expired_output_keeps_user_disable(event, subaru_native_pa
def test_actual_fixed_angle_driver_handoff_keeps_native_rate_limit(candidate, subaru_native_pair, tmp_path, preferences=None):
test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(
candidate, subaru_native_pair, tmp_path, preferences=preferences, fixed_angle_handoff=True)
@pytest.mark.parametrize('candidate', [CAR.SUBARU_IMPREZA_2020, CAR.SUBARU_OUTBACK_2023, CAR.SUBARU_CROSSTREK_2025])
def test_actual_host_first_stop_retains_subaru_intent(candidate, subaru_native_pair, tmp_path):
test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(
candidate, subaru_native_pair, tmp_path, main_only=True, stop_resume=True, host_first_stop=True)
@pytest.mark.parametrize('candidate', [CAR.SUBARU_IMPREZA_2020, CAR.SUBARU_OUTBACK_2023, CAR.SUBARU_CROSSTREK_2025])
def test_actual_held_gas_source_outage_withdraws_and_recovers_subaru(candidate, subaru_native_pair, tmp_path):
test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(
candidate, subaru_native_pair, tmp_path, main_only=True, source_outage=True)
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "ea25a21a8aae0d7bb29b4ad9a4ac129a1a32c44066adc9211c1ef44ba4973748", "schema_version": 1}
{"protected_inventory_sha256": "4a6d1ae614b9a07311e0e5f7565ea677ca19303cfc64d7557523499b56279f52", "schema_version": 1}