Retain Ford lateral intent when cruise engages under accelerator override

This commit is contained in:
firestar5683
2026-10-22 11:06:50 -05:00
parent 3ce8e82c73
commit a3b270cbfc
7 changed files with 640 additions and 7 deletions
+3 -1
View File
@@ -156,8 +156,10 @@ static void ford_aol_rx(const CANPacket_t *msg) {
// A real ordinary cruise rising edge can arm retained lateral intent without
// requiring a historical MAIN/LKAS cycle. This never grants ordinary controls
// or longitudinal permission; the current request/heartbeat/RX gates still own TX.
// Accelerator override does not erase this lateral intent. The independent
// longitudinal permission mask continues to require released accelerator/brake.
static void ford_aol_full_engagement(bool cruise_rising) {
if (ford_aol_enabled && cruise_rising && controls_allowed && !brake_pressed && !gas_pressed &&
if (ford_aol_enabled && cruise_rising && controls_allowed && !brake_pressed &&
!relay_malfunction && aol_rx_healthy() && ford_aol_main_available && ford_aol_drive && (ford_aol_eps == 0U)) {
ford_aol_token = true;
ford_aol_token_claimed = false;
@@ -70,15 +70,13 @@ def test_cold_main_already_on_full_engagement_brake_reverse_temporary_and_explic
@pytest.mark.parametrize('word', WORDS)
@pytest.mark.parametrize('veto', ('brake', 'gas', 'eps', 'reverse', 'expired-unclaimed', 'missing-health', 'ae-off'))
@pytest.mark.parametrize('veto', ('brake', 'eps', 'reverse', 'expired-unclaimed', 'missing-health', 'ae-off'))
def test_full_engagement_never_bypasses_native_qualification(ford_native, word, veto):
n = ford_native
select(n, word, ae=0 if veto == 'ae-off' else 32)
n.pump(40, main=3)
if veto == 'missing-health':
n.safety.set_timer(3_000_000)
if veto == 'gas':
n.rx('EngVehicleSpThrottle', {'ApedPos_Pc_ActlArb': 25})
if veto == 'eps':
n.rx('EPAS_INFO', {'EPAS_Failure': 2})
if veto == 'reverse':
@@ -115,3 +113,35 @@ def test_full_engagement_token_dies_on_source_health_heartbeat_or_permanent_eps(
assert n.safety.aol_get_permission_mask() == 0
n.pump(30, main=3, mask=1)
assert n.safety.aol_get_permission_mask() == 0
@pytest.mark.parametrize('word', WORDS)
@pytest.mark.parametrize('cruise', [4, 5])
def test_ordinary_engagement_with_accelerator_preserves_only_lateral_after_stock_disengage(ford_native, word, cruise):
n = ford_native
select(n, word)
n.pump(40, main=3)
original = n.rx
def gas_rx(name, values):
if name == 'EngVehicleSpThrottle':
values = {**values, 'ApedPos_Pc_ActlArb': 25}
original(name, values)
n.rx = gas_rx
request = 3 if word & 1 else 1
n.pump(10, main=3)
n.pump(40, main=cruise, mask=request)
assert n.safety.get_controls_allowed()
assert n.safety.aol_get_permission_mask() == 1
n.pump(20, main=3, mask=request)
assert not n.safety.get_controls_allowed()
assert n.safety.aol_get_permission_mask() == 1
n.rx = original
n.pump(20, main=3, mask=1)
assert n.safety.aol_get_permission_mask() == 1
assert n.safety.aol_get_permission_mask() & 2 == 0
n.pump(10, main=0, mask=1)
assert n.safety.aol_get_permission_mask() == 0
n.pump(40, main=3, mask=0)
assert n.safety.aol_get_permission_mask() == 0
+7 -3
View File
@@ -588,12 +588,16 @@ class TransportPauseFeedback:
# This exception validates intent only. The normal vehicle-health helper
# continues to reject temporary EPS for transport and availability proofs.
from openpilot.starpilot.car.nissan.aol import temporary_eps_restriction
nissan_eps = bool(temporary_eps_restriction(cp, cs) and self.lateral_only_baseline and
from openpilot.starpilot.car.ford.aol import temporary_eps_restriction as ford_temporary_eps_restriction
stock_ford_eps = bool(ford_temporary_eps_restriction(cp, cs) and
all(not event.userDisable or event.name == log.OnroadEvent.EventName.pcmDisable and not event.softDisable and
not event.immediateDisable for event in events))
restricted_eps = bool((temporary_eps_restriction(cp, cs) or stock_ford_eps) and self.lateral_only_baseline and
not axis.desiredLongitudinal and not axis.longitudinalActive)
native = _configured_transport_native(sm, cp, self.session, now_ns, allow_restricted_lateral=nissan_eps)
native = _configured_transport_native(sm, cp, self.session, now_ns, allow_restricted_lateral=restricted_eps)
if (native is None or not native.pandaInventory or
held_original and not (native.requestedLateral and (native.lateralAllowed or
nissan_eps and not native.requestedLongitudinal and not native.longitudinalAllowed)) or
restricted_eps and not native.requestedLongitudinal and not native.longitudinalAllowed)) or
axis.longitudinalActive and not native.longitudinalAllowed or
native.observedMonoTime < self.lateral_native_ns or
(native.pandaSerial, tuple(p.hardwareSerial for p in native.pandaInventory)) != self.lateral_inventory):
+14
View File
@@ -72,3 +72,17 @@ class FordCardIntent(AolCardIntent):
def create_intent(cp, settings):
return FordCardIntent(cp, settings)
def temporary_eps_restriction(cp, state):
"""Exact stock Ford physical EPS pause; intent only, no native permission."""
try:
return bool(qualified(cp, marked_only=True) and not cp.openpilotLongitudinalControl and
state.steerFaultTemporary and not state.steerFaultPermanent and state.canValid and not state.canTimeout and
state.cruiseState.available and not state.cruiseState.enabled and
state.gearShifter == structs.CarState.GearShifter.drive and
not state.brakePressed and not state.regenBraking and not state.gasPressed and
not state.doorOpen and not state.seatbeltUnlatched and not state.accFaulted and
not state.vehicleSensorsInvalid and not state.steeringDisengage and not state.buttonEvents)
except (AttributeError, KeyError, IndexError, TypeError, ValueError, OverflowError):
return False
@@ -0,0 +1,473 @@
"""Shared actual Ford factory/Card/SD/Controls/native regression fixture.
Transport, clocks and initialized onroad service sources are synthetic. This is
not startup/USB/road qualification. No desired/allowed axis value is invented.
"""
from types import SimpleNamespace
from opendbc.can import CANParser
from opendbc.car import Bus
from unittest.mock import patch
import pytest
from openpilot.starpilot.aol.wire import decode_intent
"""Test transport subset of pandad's current, producer-bound retained-arm heartbeat."""
def retained_arm(intent_event, axis_event, now_ns):
if intent_event is None or axis_event is None or not intent_event.valid or not axis_event.valid:
return False
intent = decode_intent(bytes(intent_event.aolIntentWire))
axis = axis_event.aolAxisState
if intent is None:
return False
stamp = int(intent_event.logMonoTime)
# The coupled probe has exact same-source Card/SD publications. This stricter
# subset needs none of pandad's optional newer-source/expired-axis joins.
return bool(
axis.qualified
and axis.sessionId
and axis.intentProducerSessionId
and int(axis_event.logMonoTime) == axis.observedMonoTime == axis.sourceCarStateMonoTime == stamp
and 0 < stamp <= now_ns <= axis.validUntilMonoTime
and now_ns - stamp <= 30_000_000
and intent.producerSessionId == str(axis.intentProducerSessionId)
and intent.sequence > 0
and intent.carStateLogMonoTime == intent.observedMonoTime == stamp
and intent.settingsQualified
and intent.lateralArmed
and now_ns <= intent.validUntilMonoTime
)
from opendbc.car.ford.values import CAR, FordFlags, DBC
from openpilot.cereal import messaging
from openpilot.selfdrive.car import card as card_module
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, encode_safety
FORD_FACTORIES = (
(CAR.FORD_BRONCO_SPORT_MK1, 32),
(CAR.FORD_EDGE_MK2, 8),
(CAR.FORD_ESCAPE_MK4, 32),
(CAR.FORD_ESCAPE_MK4_5, 66),
(CAR.FORD_EXPLORER_MK6, 32),
(CAR.FORD_EXPEDITION_MK4, 66),
(CAR.FORD_F_150_MK14, 66),
(CAR.FORD_F_150_LIGHTNING_MK1, 66),
(CAR.FORD_FOCUS_MK4, 32),
(CAR.FORD_MONDEO_MK5, 10),
(CAR.FORD_MAVERICK_MK1, 32),
(CAR.FORD_MUSTANG_MACH_E_MK1, 18),
(CAR.FORD_RANGER_MK2, 66),
(CAR.FORD_TRANSIT_MK5, 12),
)
@pytest.fixture
def ford_joined(identity, alpha, hands_free, monkeypatch, record_property):
from opendbc.car import car_helpers, gen_empty_fingerprint, structs
from opendbc.safety.tests import test_ford_aol as ford_native_module
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
monkeypatch.setenv('SIMULATION', '1')
monkeypatch.setenv('REPLAY', '1')
monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0')
native = ford_native_module.TestFordAolDriverIntent(methodName='runTest')
native.setUp()
release = native.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
host = None
with OpenpilotPrefix():
saved = Params()
for key, value in (
('OpenpilotEnabledToggle', True),
('AlwaysOnLateral', True),
('SafeMode', False),
('AlphaLongitudinalEnabled', alpha),
('IsReleaseBranch', release),
('FordHumanTurnDetection', False),
):
saved.put_bool(key, value, block=True)
from pathlib import Path
for key, value in (('FordLateralMode', b'1'), ('PauseAOLOnBrake', b'0.0'), ('CancelButtonControl', b'0'), ('MainCruiseButtonControl', b'0')):
Path(saved.get_param_path(key)).write_bytes(value)
from pathlib import Path
Path(saved.get_param_path("FordHandsFreeCluster")).write_bytes(b"1" if hands_free else b"0")
fp = gen_empty_fingerprint()
fp[0][0x5A] = 8
fp[0].update({0x3A6: 8, 0x3A7: 8})
fp[2].update({0x3D6: 8, 0x186: 8})
name, base = identity
observed = (name, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, True)
try:
with monkeypatch.context() as discovery:
discovery.setattr(car_helpers, 'fingerprint', lambda *args: observed)
discovery.setattr(card_module, 'can_comm_callbacks', lambda *args: (lambda *args: [], lambda frames: None))
discovery.setattr(card_module.messaging, 'recv_one_retry', lambda socket: messaging.new_message('can', 1))
host = card_module.Car()
assert host.CP.alternativeExperience == 32 and host.aol_card_intent is not None
assert str(host.CP.carFingerprint) == str(name)
expected_long = bool(alpha and not release)
assert host.CP.openpilotLongitudinalControl == expected_long
assert host.CP.safetyConfigs[0].safetyParam == base | int(expected_long)
record_property('ford_factory_identity', str(name))
record_property('ford_saved_alpha', alpha)
record_property('ford_release', release)
record_property('ford_longitudinal', expected_long)
record_property('ford_safety_word', int(host.CP.safetyConfigs[0].safetyParam))
native.reset(host.CP.safetyConfigs[0].safetyParam)
native.word &= ~1
# Real empty update registers lazily accessed parser messages before RX.
# Card.controls_update owns CI initialization; no flag is assigned here.
host.CI.update([])
for _warm_tick in range(120):
preframes = []
def pre_rx(message, fields, preframes=preframes):
frame = native.packer.make_can_msg(message, 0, fields)
from opendbc.safety.tests.libsafety import libsafety_py
assert native.safety.safety_rx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))
preframes.append(frame)
with patch.object(native, 'rx', side_effect=pre_rx):
native.pump(1, main=3, mask=0, heartbeat=False, speed=15.0)
extras = [
('SteeringPinion_Data', 0, {'StePinCompAnEst_D_Qf': 3}),
('Cluster_Info1_FD1', 0, {'AccEnbl_B_RqDrv': 1}),
('BodyInfo_3_FD1', 0, {}),
('RCMStatusMessage2_FD1', 0, {'FirstRowBuckleDriver': 1}),
('INSTRUMENT_PANEL', 0, {}),
('IPMA_Data', 2, {}),
('ACCDATA', 2, {}),
('ACCDATA_2', 2, {}),
('ACCDATA_3', 2, {}),
]
if host.CP.flags & FordFlags.ALT_STEER_ANGLE:
extras += [('SteeringPinion_Data_Alt', 0, {'StePinRelInit_An_Sns': 0}), ('ParkAid_Data', 0, {'ExtSteeringAngleReq2': 0})]
if host.CP.flags & FordFlags.LKA_STEERING:
extras += [('LateralMotionControl', 2, {})]
extras += [('Side_Detect_L_Stat', 2, {}), ('Side_Detect_R_Stat', 2, {})]
preframes += [native.packer.make_can_msg(message, bus, fields) for message, bus, fields in extras]
state = host.CI.update([(10_000_000_000 + native.tick * 10_000_000, preframes)])
assert not native.safety.get_controls_allowed() and native.safety.aol_get_permission_mask() == 0
assert state.canValid and not state.canTimeout
yield SimpleNamespace(host=host, native=native, safety=native.safety, initial_cp=host.CP.to_dict(), expected_long=expected_long, hands_free=hands_free)
finally:
native.tearDown()
if host is not None:
host.vehicle_startup.close()
def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengage=False):
# Reuse the factory/parser/native configuration; no old synthetic SD peer runs.
from openpilot.starpilot.aol.tests import test_ioniq_availability as availability
joined = ford_joined
host, native, safety = joined.host, joined.native, joined.safety
case = availability.IoniqAvailabilityTests(methodName='runTest')
now = [10_000_000_000]
queued, published, previous_sd = [], {}, {}
steps = start_tick = native.tick
warmup_waits = qualified_samples = 0
active_lat_frames = active_long_frames = 0
long_parser = CANParser(DBC[host.CP.carFingerprint][Bus.pt], [('ACCDATA', 50)], 0)
class ClockMappedSafety:
"""Translate only the native fixture's clock epoch to the existing SD harness."""
def set_timer(self, timestamp):
safety.set_timer(timestamp + 9_000_000)
def __getattr__(self, name):
return getattr(safety, name)
try:
with monkeypatch.context() as local:
local.setenv('SIMULATION', '0')
sd, controls, drive = case._authority_callers(False, cp=host.CP.as_reader().as_builder(), genuine_events=True)
assert sd.CP.to_dict() == controls.CP.to_dict() == host.CP.to_dict()
local.setattr(card_module, 'REPLAY', False)
local.setattr(card_module.time, 'monotonic_ns', lambda: now[0])
local.setattr(card_module.time, 'clock_gettime_ns', lambda clock: now[0])
local.setattr(card_module.time, 'monotonic', lambda: now[0] / 1e9)
local.setattr(card_module.messaging, 'drain_sock_raw', lambda *args, **kwargs: list(queued))
local.setattr(host.sm, 'update', lambda *args: None)
create = messaging.new_message
def clocked_message(service=None, size=None, **kwargs):
kwargs.setdefault('logMonoTime', now[0])
return create(service, size, **kwargs)
local.setattr(messaging, 'new_message', clocked_message)
send = host.pm.send
def capture(service, event):
raw = event if isinstance(event, bytes) else event.to_bytes()
published[service] = messaging.log_from_bytes(raw).as_builder()
return send(service, raw)
local.setattr(host.pm, 'send', capture)
native.safety = ClockMappedSafety()
location_unavailable = [False]
original_update = sd.sm.update_msgs
def location_update(timestamp, messages):
selected = []
for message in messages:
if message.which() == 'deviceMotion':
message = message.as_builder()
message.deviceMotion.inputsOK = not location_unavailable[0]
message = message.as_reader()
selected.append(message)
return original_update(timestamp, selected)
local.setattr(sd.sm, 'update_msgs', location_update)
def step(*, engaged=False, brake=False, reverse=False, warmup=False, fault=None, main_off=False):
nonlocal steps, warmup_waits, qualified_samples, active_lat_frames, active_long_frames
steps += 1
now[0] = 10_000_000_000 + steps * 10_000_000
prior_axis = previous_sd.get('aolAxisState')
joined.last_input_axis = prior_axis
prior_state = previous_sd.get('selfdriveState')
request = 0 if prior_axis is None else (int(prior_axis.aolAxisState.desiredLateral) | (int(prior_axis.aolAxisState.desiredLongitudinal) << 1))
arm_heartbeat = retained_arm(published.get('aolIntentWire'), prior_axis, now[0])
heartbeat = bool(request or prior_state is not None and prior_state.selfdriveState.enabled or arm_heartbeat)
frames = []
def receive(name, values):
fields = dict(values)
if name == 'EngBrakeData' and brake:
fields['BpedDrvAppl_D_Actl'] = 2
if name == 'EngVehicleSpThrottle' and fault == 'gas':
fields['ApedPos_Pc_ActlArb'] = 25
if name == 'BrakeSysFeatures' and fault == 'rx-quality':
fields['VehVActlBrk_D_Qf'] = 0
frame = native.packer.make_can_msg(name, 0, fields)
if not (fault == 'source-loss' and name == 'Yaw_Data_FD1'):
accepted = safety.safety_rx_hook(joined_packet(frame))
assert bool(accepted) == (not (fault == 'rx-quality' and name == 'BrakeSysFeatures'))
frames.append(frame)
with patch.object(native, 'rx', side_effect=receive):
native.pump(
1,
main=0 if main_off else 5 if engaged else 3,
gear=1 if reverse else 3,
mask=request,
heartbeat=heartbeat and fault != 'heartbeat',
speed=15.0,
eps=1 if fault == 'temporary-eps' else 0,
)
assert native.tick == steps
safety.safety_tick()
native_request = safety.aol_get_request_mask()
permission = safety.aol_get_permission_mask()
joined.last_permission = permission
joined.last_native_request = native_request
if prior_axis is None:
assert request == native_request == permission == 0
assert permission & 1 == 0 or heartbeat and request & 1 and native_request & 1
extras = [
('SteeringPinion_Data', 0, {'StePinCompAnEst_D_Qf': 3}),
('Cluster_Info1_FD1', 0, {'AccEnbl_B_RqDrv': 1}),
('BodyInfo_3_FD1', 0, {}),
('RCMStatusMessage2_FD1', 0, {'FirstRowBuckleDriver': 1}),
('INSTRUMENT_PANEL', 0, {}),
('IPMA_Data', 2, {}),
('ACCDATA', 2, {}),
('ACCDATA_2', 2, {}),
('ACCDATA_3', 2, {}),
]
if host.CP.flags & FordFlags.ALT_STEER_ANGLE:
extras.append(('SteeringPinion_Data_Alt', 0, {'StePinRelInit_An_Sns': 0}))
if steps % 2 == 0:
extras.append(('ParkAid_Data', 0, {'ExtSteeringAngleReq2': 0}))
if host.CP.flags & FordFlags.LKA_STEERING and steps % 5 == 0:
extras.append(('LateralMotionControl', 2, {}))
extras += [('Side_Detect_L_Stat', 2, {}), ('Side_Detect_R_Stat', 2, {})]
frames += [native.packer.make_can_msg(name, bus, fields) for name, bus, fields in extras]
batch = messaging.log_from_bytes(card_module.can_list_to_can_capnp(frames)).as_builder()
batch.logMonoTime = now[0]
queued[:] = [batch.to_bytes()]
config = host.CP.safetyConfigs[0]
slot = PandaSlot(
0, 'authority-panda', config.safetyModel.raw, config.safetyParam, 32, bool(safety.get_controls_allowed()), not bool(safety.safety_config_valid())
)
native_state = SafetyState(
1,
True,
now[0],
now[0] + 200_000_000,
config.safetyModel.raw,
config.safetyParam,
bool(permission & 1),
bool(permission & 2),
bool(native_request & 1),
bool(native_request & 2),
'authority-panda',
str(prior_axis.aolAxisState.sessionId) if prior_axis is not None else host.aol_axis_session(),
sourcePandaStatesMonoTime=now[0],
pandaInventory=(slot,),
)
wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now[0])
wire.aolSafetyWire = encode_safety(native_state)
panda = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=now[0])
panda.pandaStates[0].safetyModel = config.safetyModel
panda.pandaStates[0].safetyParam = config.safetyParam
panda.pandaStates[0].alternativeExperience = 32
panda.pandaStates[0].controlsAllowed = slot.controlsAllowed
panda.pandaStates[0].safetyRxChecksInvalid = slot.safetyRxChecksInvalid
device = messaging.new_message('deviceState', valid=True, logMonoTime=now[0])
device.deviceState.started, device.deviceState.startedMonoTime = True, 9_000_000_000
events = [wire.as_reader(), panda.as_reader(), device.as_reader()]
for name in ('selfdriveState', 'aolAxisState', 'onroadEvents'):
if name in previous_sd:
events.append(previous_sd[name]) # Original publication/source stamps.
if 'onroadEvents' not in previous_sd:
events.append(messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now[0]).as_reader())
host.sm.update_msgs(now[0] / 1e9, events)
state, _ = host.state_update()
host.state_publish(state, None)
assert 'aolIntentWire' in published
parser_ready = bool(
state.canValid and not state.canTimeout and state.cruiseState.available and str(state.gearShifter) == 'drive' and safety.safety_config_valid()
)
if warmup and not parser_ready:
assert steps <= start_tick + 120 and not engaged and not brake and not reverse
assert request == native_request == permission == 0
assert not safety.get_controls_allowed() and not host.aol_card_intent.allowed_latch
warmup_waits += 1
return state, None, None, None
if fault is None:
assert state.canValid and not state.canTimeout
qualified_samples += 1
parsed_before_producer = state.to_dict()
assert (main_off or state.cruiseState.available) and not state.steeringPressed
assert bool(state.gasPressed) == (fault == 'gas')
axis, sd_state, command = drive(
steps,
car_state_message=published['carState'],
gas=bool(state.gasPressed),
steering=bool(state.steeringPressed),
intent_message=published['aolIntentWire'],
allowed=bool(permission & 1),
long_allowed=bool(permission & 2),
native_request_lat=bool(native_request & 1),
native_request_long=bool(native_request & 2),
panda_controls_allowed=bool(safety.get_controls_allowed()),
panda_fault=slot.safetyRxChecksInvalid,
)
assert state.to_dict() == parsed_before_producer, 'Authority harness changed actual parsed CarState'
previous_sd.update(sd.pm.messages)
control = controls.pm.messages['carControl']
source_ns = now[0]
now[0] += 1
host.sm.update_msgs(now[0] / 1e9, [control])
host.can_log_mono_time = source_ns
published.pop('sendcan', None)
host.controls_update(state, host.sm['carControl'])
if state.canValid:
assert 'sendcan' in published
assert published['sendcan'].valid
for frame in published['sendcan'].sendcan:
from opendbc.safety.tests.libsafety import libsafety_py
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(frame.address, frame.src, frame.dat))
data, address = frame.dat, frame.address
if address in (0x3CA, 0x3D3, 0x3D6):
active = ((data[4] >> 2) & 7) != 0 if address == 0x3D3 else ((data[0] >> 4) & 7) != 0 if address == 0x3D6 else (data[0] >> 5) in (2, 4)
if active:
assert command.latActive and permission & 1
active_lat_frames += 1
elif address == 0x186:
long_parser.update([(source_ns, [(address, bytes(data), frame.src)])])
active = bool(long_parser.vl['ACCDATA']['Cmbb_B_Enbl'])
assert active == bool(command.longActive)
active_long_frames += int(active)
if scenario in ('display', 'route24'):
ui = [f for f in published['sendcan'].sendcan if f.address in (0x3D8, 0x18A)]
owns = all(safety.safety_fwd_hook(2, address) == -1 for address in (0x3D8, 0x18A))
assert host.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not ui or owns, (
'Host injected cluster UI while native still forwarded stock UI',
steps,
bool(command.latActive),
int(permission),
int(safety.aol_get_permission_mask()),
[(hex(f.address), bytes(f.dat).hex()) for f in published['sendcan'].sendcan],
dict(vars(host.CI.CC)).get('aol_mache_ui'),
)
for frame in ui:
if frame.address == 0x18A and command.latActive:
display_parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
display_parser.update([(now[0], [(frame.address, bytes(frame.dat), frame.src)])])
assert display_parser.vl['ACCDATA_3']['Tja_D_Stat'] == (7 if joined.hands_free else 2)
if not command.latActive and host.CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1:
assert not ui
assert all(safety.safety_fwd_hook(2, address) == 0 for address in (0x3D8, 0x18A))
assert bool(axis.longitudinalActive) == bool(command.longActive)
if not joined.expected_long:
assert not axis.longitudinalActive and not command.longActive
elif command.longActive:
assert permission & 2 and safety.get_controls_allowed()
return state, axis, sd_state, command
def joined_packet(frame):
from opendbc.safety.tests.libsafety import libsafety_py
return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])
for _ in range(120):
step(warmup=True)
assert 0 <= warmup_waits < 120 and qualified_samples >= 100
assert host.ci_initialized # Read-only witness: real controls_update completed init.
assert not host.aol_card_intent.allowed_latch
assert scenario == 'route24'
if scenario == 'route24':
assert host.CP.flags == 3 and not host.CP.openpilotLongitudinalControl
for _ in range(30):
step(fault='gas')
for _ in range(40):
state, axis, sd_state, command = step(engaged=True, fault='gas')
assert state.cruiseState.enabled and state.gasPressed and host.aol_card_intent.allowed_latch
for _ in range(40):
state, axis, sd_state, command = step()
assert host.aol_card_intent.allowed_latch, 'Route24 normal stock disengage after gas engagement erased AOL'
assert axis.lateralActive and command.latActive and not command.longActive and not sd_state.enabled
for _ in range(40):
step(engaged=True)
for _ in range(30):
state, axis, sd_state, command = step(brake=True)
assert host.aol_card_intent.allowed_latch and command.latActive and not command.longActive
for _ in range(20):
state, axis, sd_state, command = step()
assert not state.brakePressed and command.latActive and host.aol_card_intent.allowed_latch
for _ in range(60):
state, axis, sd_state, command = step(fault='temporary-eps')
assert host.aol_card_intent.allowed_latch
if state.steerFaultTemporary:
assert not command.latActive and not command.longActive
for _ in range(40):
state, axis, sd_state, command = step()
assert command.latActive and host.aol_card_intent.allowed_latch and not command.longActive
for _ in range(30):
state, axis, sd_state, command = step(reverse=True)
assert host.aol_card_intent.allowed_latch and not command.latActive and not command.longActive
for _ in range(40):
state, axis, sd_state, command = step()
assert command.latActive and not command.longActive and host.aol_card_intent.allowed_latch
for _ in range(30):
state, axis, sd_state, command = step(main_off=True)
assert not state.cruiseState.available
assert not host.aol_card_intent.allowed_latch and not command.latActive and not command.longActive
return
finally:
native.safety = safety
case.doCleanups()
@@ -0,0 +1,16 @@
"""Route0a13721d7a3cb316/00000024--f79244cffe: Mach-E stock ACC, flags3, AE32.
Captured configuration and event order; synthetic CAN/perception/USB transport.
No VIN, location or native permission is injected. Physical acceptance is separate.
"""
import pytest
from opendbc.car.ford.values import CAR
from openpilot.starpilot.tests.ford_lifecycle_helpers import ford_joined as ford_joined, run_actual_producer
@pytest.mark.parametrize('identity', [(CAR.FORD_MUSTANG_MACH_E_MK1, 18)])
@pytest.mark.parametrize('alpha', [False])
@pytest.mark.parametrize('hands_free', [False, True])
def test_route24_accelerator_at_engagement_full_brake_stock_off_fault_reverse_and_main_off(ford_joined, monkeypatch):
run_actual_producer(ford_joined, monkeypatch, scenario='route24')
@@ -0,0 +1,94 @@
"""Stock Ford's native temporary EPS denial withdraws output, not existing intent."""
from dataclasses import replace
import pytest
from openpilot.cereal import messaging
from openpilot.starpilot.tests.test_ford_location_intent import IDENTITIES, location_case
from openpilot.starpilot.tests.test_ford_reverse_intent import NOW, PRODUCER, change, native_change, sample
def case(identity=IDENTITIES[0]):
cp, cs, feedback, _, _, _ = location_case(identity, False)
stamp = NOW + 10_000_000
peers, native = sample(cp, stamp, stamp, 2, allowed=False)
cs.steerFaultTemporary = True
return cp, cs, feedback, peers, native, stamp
@pytest.mark.parametrize('identity', IDENTITIES)
def test_stock_temporary_eps_join_preserves_only_original_intent_and_recovers(identity):
cp, cs, feedback, peers, _, stamp = case(identity)
baseline = (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
stamps = dict(peers.logMonoTime)
assert not feedback.observe(peers, cp, cs, stamp, latched=True, intent_producer_session=PRODUCER)
assert not feedback.failed
assert baseline == (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
assert peers.logMonoTime == stamps
next_stamp = stamp + 10_000_000
withdrawn, _ = sample(cp, next_stamp, next_stamp, 3, active=False, requested=False, allowed=False, event_stamp=next_stamp)
change(withdrawn, 'aolAxisState', lambda axis: setattr(axis, 'nativeAcknowledged', False))
event = messaging.new_message('onroadEvents', 2, valid=True, logMonoTime=next_stamp)
event.onroadEvents = [{'name': 'pcmDisable', 'userDisable': True}, {'name': 'steerTempUnavailableSilent', 'warning': True}]
withdrawn.messages['onroadEvents'] = event.as_reader()
withdrawn.logMonoTime['onroadEvents'] = next_stamp
assert not feedback.observe(withdrawn, cp, cs, next_stamp, latched=True, intent_producer_session=PRODUCER)
assert feedback.temporary_eps_event_ns == next_stamp
assert baseline == (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
cs.steerFaultTemporary = False
recovered, _ = sample(cp, next_stamp + 10_000_000, next_stamp + 10_000_000, 4, event_stamp=next_stamp + 10_000_000)
assert not feedback.observe(recovered, cp, cs, next_stamp + 10_000_000, latched=True, intent_producer_session=PRODUCER)
assert not feedback.failed and feedback.lateral_source_ns == next_stamp + 10_000_000
@pytest.mark.parametrize('fault', [
'no-eps', 'permanent-eps', 'can-invalid', 'timeout', 'brake', 'gas', 'regen', 'door', 'seatbelt',
'reverse', 'main-off', 'not-latched', 'producer', 'epoch', 'session', 'expired', 'stale-source',
'no-baseline', 'native-rx', 'native-heartbeat', 'native-fault', 'native-config', 'native-long',
'axis-long', 'hard-event', 'cancel',
])
def test_stock_eps_cannot_hide_independent_fault_or_borrow_another_owner(fault):
cp, cs, feedback, peers, native, stamp = case()
latched, producer = True, PRODUCER
if fault in ('no-eps', 'permanent-eps', 'can-invalid', 'timeout', 'brake', 'gas', 'regen', 'door', 'seatbelt'):
field = {'no-eps': 'steerFaultTemporary', 'permanent-eps': 'steerFaultPermanent', 'can-invalid': 'canValid',
'timeout': 'canTimeout', 'brake': 'brakePressed', 'gas': 'gasPressed', 'regen': 'regenBraking',
'door': 'doorOpen', 'seatbelt': 'seatbeltUnlatched'}[fault]
setattr(cs, field, fault not in ('no-eps', 'can-invalid'))
elif fault == 'reverse':
cs.gearShifter = 'reverse'
elif fault == 'main-off':
cs.cruiseState.available = False
elif fault == 'not-latched':
before = dict(vars(feedback))
assert not feedback._temporary_steering_intent_current(
peers, cp, cs, peers['aolAxisState'], stamp, stamp, latched=False, producer=producer, signature=feedback.signature)
assert vars(feedback) == before
return
elif fault == 'producer':
producer = 'other-card'
elif fault in ('epoch', 'session', 'stale-source', 'axis-long'):
field, value = {'epoch': ('intentIntegrityEpoch', 2), 'session': ('sessionId', 'other-axis'),
'stale-source': ('sourceCarStateMonoTime', stamp - 30_000_001),
'axis-long': ('desiredLongitudinal', True)}[fault]
change(peers, 'aolAxisState', lambda axis: setattr(axis, field, value))
elif fault == 'expired':
stamp += 200_000_001
elif fault == 'no-baseline':
feedback.lateral_only_baseline = False
elif fault == 'native-config':
native_change(peers, native, safetyParam=0)
elif fault == 'native-long':
native_change(peers, native, requestedLongitudinal=True)
elif fault in ('native-rx', 'native-heartbeat', 'native-fault'):
updates = {'native-rx': {'safetyRxChecksInvalid': True}, 'native-heartbeat': {'heartbeatLost': True},
'native-fault': {'faults': 8}}[fault]
native_change(peers, native, pandaInventory=(replace(native.pandaInventory[0], **updates),))
elif fault == 'hard-event':
change(peers, 'onroadEvents', lambda events: setattr(events[0], 'immediateDisable', True))
elif fault == 'cancel':
change(peers, 'onroadEvents', lambda events: setattr(events[0], 'name', 'buttonCancel'))
assert feedback.observe(peers, cp, cs, stamp, latched=latched, intent_producer_session=producer)
assert feedback.failed